Angular 20 Firebase Tutorial: Build a Firestore CRUD Web App with Standalone Components

by Didin J. on Aug 17, 2025 Angular 20 Firebase Tutorial: Build a Firestore CRUD Web App with Standalone Components

Build a CRUD Firestore app with Angular 20 standalone components, signals, and modern template syntax — no modules, faster reactivity, cleaner code.

Building web applications with Angular has come a long way since version 6. Back then, developers relied heavily on NgModules, verbose boilerplate, and a less streamlined integration with Firebase. Fast-forward to today, Angular 20 introduces a new era of simplicity and power with standalone components, signals-based reactivity, and modern template syntax like @if and @for.

In this tutorial, we’ll modernize the classic Angular + Firebase Firestore CRUD app using the latest Angular 20 features. You’ll learn how to:

  • Set up an Angular 20 project with standalone components (no NgModules).

  • Connect your app to Firebase Firestore using the AngularFire library.

  • Implement full CRUD (Create, Read, Update, Delete) functionality.

  • Use signals and the new template control flow for cleaner, more reactive code.

By the end, you’ll have a fast, maintainable, and future-proof Firestore CRUD application built with the latest Angular practices.


Setup & Project Initialization (Angular 20 + Firebase)

1. Install Node.js and Angular CLI

Ensure you have the latest Node.js LTS version installed (recommended:≥ v20). Then install Angular CLI globally:

npm install -g @angular/cli

Verify the version:

ng version

It should show Angular CLI: 20.x.x.

2. Create a New Angular 20 Project

Generate a new Angular project using the standalone mode flag:

ng new angular20-firestore --standalone
cd angular20-firestore

This will scaffold a new Angular app where the root component (AppComponent) is standalone.
✅ No AppModule is generated — everything starts from main.ts.

3. Set Up Firebase Project

  1. Go to Firebase Console.

  2. Create a new project (or use an existing one).

  3. Enable Firestore Database in Production mode.

  4. Copy your project’s Firebase config object (from Project Settings → SDK setup and configuration). It looks like:

export const environment = {
  firebase: {
    apiKey: "AIzaSyxxxxxx...",
    authDomain: "your-app.firebaseapp.com",
    projectId: "your-app",
    storageBucket: "your-app.appspot.com",
    messagingSenderId: "1234567890",
    appId: "1:1234567890:web:abcdef123456"
  }
};

4. Install Firebase & AngularFire

Add Firebase SDK and AngularFire library:

npm install firebase @angular/fire

5. Configure Firebase in Angular

Open src/app/app.config.ts and import Firebase providers inside the bootstrapApplication function:

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideFirestore(() => getFirestore()),
  ]
};

6. Create src/environments/environment.ts

export const environment = {
  production: true,
  firebase: {
    apiKey: "AIzaSyxxxxxx...",
    authDomain: "your-app.firebaseapp.com",
    projectId: "your-app",
    storageBucket: "your-app.appspot.com",
    messagingSenderId: "1234567890",
    appId: "1:1234567890:web:abcdef123456"
  }
};

This sets up Firebase and Firestore globally in your Angular 20 app.

✅ At this point, you have:

  • A brand-new Angular 20 project with standalone components.

  • Firebase + Firestore is integrated and ready to use.


Creating the Firestore Service (CRUD Operations)

We’ll create a service that interacts with Firestore using the modular Firebase SDK and AngularFire’s helpers.

1. Create the Service File

Generate a new service:

ng generate service services/firestore.service

This will create src/app/services/firestore.service.ts.

2. Implement Firestore CRUD Methods

Open firestore.service.ts and update it:

import { Injectable } from '@angular/core';
import { collectionData, docData } from '@angular/fire/firestore';
import { collection, doc, addDoc, updateDoc, deleteDoc, Firestore } from 'firebase/firestore';
import { Observable } from 'rxjs';

export interface Board {
  id?: string;      // Firestore document ID
  title: string;
  description: string;
  author: string;
}

@Injectable({
  providedIn: 'root'
})
export class FirestoreService {
  constructor(private firestore: Firestore) { }

  // Get all boards (Realtime stream)
  getBoards(): Observable<Board[]> {
    const boardsRef = collection(this.firestore, 'boards');
    return collectionData(boardsRef, { idField: 'id' }) as Observable<Board[]>;
  }

  // Get a single board by ID
  getBoard(id: string): Observable<Board> {
    const boardDocRef = doc(this.firestore, `boards/${id}`);
    return docData(boardDocRef, { idField: 'id' }) as Observable<Board>;
  }

  // Add a new board
  addBoard(board: Board) {
    const boardsRef = collection(this.firestore, 'boards');
    return addDoc(boardsRef, board);
  }

  // Update an existing board
  updateBoard(board: Board) {
    const boardDocRef = doc(this.firestore, `boards/${board.id}`);
    return updateDoc(boardDocRef, {
      title: board.title,
      description: board.description,
      author: board.author
    });
  }

  // Delete a board
  deleteBoard(id: string) {
    const boardDocRef = doc(this.firestore, `boards/${id}`);
    return deleteDoc(boardDocRef);
  }
}

3. What This Service Does

  • getBoards() → Returns a live-updating list of all documents in boards.

  • getBoard(id) → Retrieves a single board document.

  • addBoard(board) → Adds a new board to Firestore.

  • updateBoard(board) → Updates an existing board by ID.

  • deleteBoard(id) → Deletes a board by ID.

✅ At this point, you have a reusable Firestore service ready to plug into your standalone components for CRUD functionality.


Creating Standalone CRUD Components (Add, List, Edit)

We’ll build three main components:

  1. Board List → display all boards

  2. Board Add → form to add a new board

  3. Board Edit → form to edit an existing board

1. Generate Components

Run the following commands:

ng generate component components/board-list --standalone --flat
ng generate component components/board-add --standalone --flat
ng generate component components/board-edit --standalone --flat

This will create:

  • src/app/components/board-list.component.ts

  • src/app/components/board-add.component.ts

  • src/app/components/board-edit.component.ts

2. Board List Component

board-list.ts

import { Component, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { FirestoreService, Board } from '../services/firestore.service';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-board-list',
  imports: [RouterModule, CommonModule],
  templateUrl: './board-list.html',
  styleUrl: './board-list.scss'
})
export class BoardList {
  firestore = inject(FirestoreService);
  boards$: Observable<Board[]> = this.firestore.getBoards();

  deleteBoard(id: string) {
    this.firestore.deleteBoard(id);
  }
}

board-list.html

<div class="container">
  <h2>Boards</h2>
  <button routerLink="/add" class="btn btn-primary">Add Board</button>
  <ul class="list-group mt-3">
    <li
      *ngFor="let board of boards$ | async"
      class="list-group-item d-flex justify-content-between align-items-center"
    >
      <span>
        <strong>{{ board.title }}</strong> - {{ board.description }}
      </span>
      <span>
        <button [routerLink]="['/edit', board.id]" class="btn btn-sm btn-warning me-2">Edit</button>
        <button (click)="deleteBoard(board.id!)" class="btn btn-sm btn-danger">Delete</button>
      </span>
    </li>
  </ul>
</div>

3. Board Add Component

board-add.ts

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { FirestoreService, Board } from '../services/firestore.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-board-add',
  imports: [CommonModule, FormsModule],
  templateUrl: './board-add.html',
  styleUrl: './board-add.scss'
})
export class BoardAdd {
  firestore = inject(FirestoreService);
  router = inject(Router);

  board: Board = { title: '', description: '', author: '' };

  async addBoard() {
    await this.firestore.addBoard(this.board);
    this.router.navigate(['/']);
  }
}

board-add.html

<div class="container">
  <h2>Add Board</h2>
  <form (ngSubmit)="addBoard()">
    <div class="mb-3">
      <label class="form-label">Title</label>
      <input [(ngModel)]="board.title" name="title" class="form-control" required />
    </div>
    <div class="mb-3">
      <label class="form-label">Description</label>
      <textarea
        [(ngModel)]="board.description"
        name="description"
        class="form-control"
        required
      ></textarea>
    </div>
    <div class="mb-3">
      <label class="form-label">Author</label>
      <input [(ngModel)]="board.author" name="author" class="form-control" required />
    </div>
    <button type="submit" class="btn btn-success">Save</button>
  </form>
</div>

4. Board Edit Component

board-edit.ts

import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs';
import { FirestoreService, Board } from '../services/firestore.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-board-edit',
  imports: [CommonModule, FormsModule],
  templateUrl: './board-edit.html',
  styleUrl: './board-edit.scss'
})
export class BoardEdit {
  firestore = inject(FirestoreService);
  route = inject(ActivatedRoute);
  router = inject(Router);

  board: Board | undefined;

  constructor() {
    this.route.paramMap
      .pipe(switchMap(params => this.firestore.getBoard(params.get('id')!)))
      .subscribe(board => this.board = board);
  }

  async updateBoard() {
    if (this.board?.id) {
      await this.firestore.updateBoard(this.board);
      this.router.navigate(['/']);
    }
  }
}

board-edit.ts

@if(board) {
<div class="container">
  <h2>Edit Board</h2>
  <form (ngSubmit)="updateBoard()">
    <div class="mb-3">
      <label class="form-label">Title</label>
      <input [(ngModel)]="board.title" name="title" class="form-control" required />
    </div>
    <div class="mb-3">
      <label class="form-label">Description</label>
      <textarea
        [(ngModel)]="board.description"
        name="description"
        class="form-control"
        required
      ></textarea>
    </div>
    <div class="mb-3">
      <label class="form-label">Author</label>
      <input [(ngModel)]="board.author" name="author" class="form-control" required />
    </div>
    <button type="submit" class="btn btn-primary">Update</button>
  </form>
</div>
}

✅ With these three components, you now have:

  • List → see all boards and delete them.

  • Add → create a new board.

  • Edit → update an existing board.

Next, we need to configure routing so navigation works between them.


App Routing Setup (Standalone Router Config)

In Angular 20, routing can be defined directly with the provideRouter API and no AppRoutingModule is needed. We’ll set up routes for list, add, and edit pages.

1. Update app.routes.ts

Open src/app/app.routes.ts and update it like this:

import { Routes } from '@angular/router';
import { BoardList } from './components/board-list';
import { BoardAdd } from './components/board-add';
import { BoardEdit } from './components/board-edit';

export const routes: Routes = [
  { path: '', component: BoardList },
  { path: 'add', component: BoardAdd },
  { path: 'edit/:id', component: BoardEdit },
];

2. Add a Router Outlet in app.html

Ensure AppComponent has a <router-outlet> so routes can render inside it.

<h1 class="title">Angular 20 Firestore CRUD App</h1>
<nav>
  <a routerLink="/">Home</a> |
  <a routerLink="/add">Add Item</a>
</nav>
<router-outlet></router-outlet>

Update app.scss:

nav {
  margin: 1rem 0;
}

a {
  margin-right: 1rem;
}

✅ At this point:

  • Visiting / → shows the list of items.

  • Visiting /add → shows the add form.

  • Visiting /edit/:id → shows the edit form.


Styling the App + Testing CRUD Functionality

1. Add Global Styles

Open src/styles.css and add some simple, responsive CSS for a clean look:

/* You can add global styles to this file, and also import other style files */
body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 2rem;
  background: #f9f9f9;
}

h1.title {
  color: #1976d2;
  text-align: center;
}

nav {
  display: flex;
  justify-content: center;
  margin-bottom: 1.5rem;
}

nav a {
  text-decoration: none;
  color: #1976d2;
  font-weight: 500;
  margin: 0 1rem;
}

nav a:hover {
  text-decoration: underline;
}

form {
  max-width: 400px;
  margin: 2rem auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

input,
button {
  padding: 0.6rem;
  font-size: 1rem;
}

button {
  cursor: pointer;
  border: none;
  background-color: #1976d2;
  color: white;
  border-radius: 4px;
  transition: background 0.3s;
}

button:hover {
  background-color: #125ea7;
}

ul {
  list-style: none;
  padding: 0;
  max-width: 500px;
  margin: 2rem auto;
}

li {
  background: white;
  margin-bottom: 0.5rem;
  padding: 1rem;
  border-radius: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.actions button {
  margin-left: 0.5rem;
}

2. Enhance List View

Update list-items.component.html template for better UI:

<div class="container">
  <h2>Boards</h2>
  <button routerLink="/add" class="btn btn-primary">Add Board</button>
  <ul class="list-group mt-3">
    <li
      *ngFor="let board of boards$ | async"
      class="list-group-item d-flex justify-content-between align-items-center"
    >
      <span>
        <strong>{{ board.title }}</strong> - {{ board.description }}
      </span>
      <span>
        <button [routerLink]="['/edit', board.id]" class="btn btn-sm btn-warning me-2">Edit</button>
        <button (click)="deleteBoard(board.id!)" class="btn btn-sm btn-danger">Delete</button>
      </span>
    </li>
  </ul>
</div>

3. Testing CRUD Functionality

Now let’s test each step in the browser:

  1. Start the app

ng serve

Then go to http://localhost:4200.

  1. Test Add Item

    • Click “Add Item” in the navbar.

    • Enter a name and submit.

    • You should be redirected to the list and see your new item appear.

  2. Test Edit Item

    • Click Edit next to an item.

    • Change the name, save, and check the updated list.

  3. Test Delete Item

    • Click Delete on an item.

    • It should be removed from Firestore instantly.

  4. Firestore Console Check

    • Open the Firebase Console → Firestore Database.

    • You should see your items updating live as you add, edit, and delete.

✅ At this point, you’ve built a fully functional Angular 20 + Firebase Firestore CRUD web app with modern standalone APIs.


Conclusion

In this tutorial, you learned how to build a modern Angular 20 CRUD application with Firebase Firestore using standalone components and the latest Angular features. We covered:

  • Setting up an Angular 20 project and integrating Firebase.

  • Creating a Firestore service with full CRUD operations.

  • Building standalone components for listing, adding, and editing items.

  • Configuring standalone routing with provideRouter.

  • Styling the app with Angular Material and testing the functionality end-to-end.

The result is a cleaner, more maintainable, and future-proof app compared to older Angular versions that relied heavily on NgModules. With Angular 20, reactivity via signals, simplified template control flow, and a standalone architecture make development smoother than ever.


Next Steps

Here are a few directions you can take this project further:

  1. Authentication & Authorization

    • Secure your Firestore data by integrating Firebase Authentication (Google, GitHub, or Email/Password login).

  2. Deploy to Firebase Hosting

    • Take your app live with just a few commands using ng deploy and Firebase Hosting.

  3. Advanced Firestore Features

    • Add real-time listeners, pagination, or query-based filtering to handle larger datasets.

  4. Error Handling & Toast Notifications

    • Improve UX by showing snackbars for errors and successful CRUD actions.

  5. PWA (Progressive Web App) Support

    • Enable offline persistence with Firestore and turn this into a PWA for a native-like experience.

You can find the full working source code on our GitHub.

If you don’t want to waste your time designing your front-end or your budget to spend by hiring a web designer, then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That's just the basics. If you need more deep learning about Angular, you can take the following cheap course:

Thanks!