# Building a Full-Stack CRUD App with Angular, Spring Boot, and H2

When learning full-stack development, building a simple CRUD (Create, Read, Update, Delete) application is the best way to understand how a frontend framework communicates with a backend service and a database.

In this guide, we will build a **Bookmark Manager**. This application allows you to save web links, view them in a clean interface, update their details, and delete them when they are no longer needed.

### Architecture Overview

Our application follows a clean, decoupled client-server architecture:

*   **Frontend (Angular):** Handles user interactions, manages UI state, and sends asynchronous HTTP requests via Angular's `HttpClient`.
    
*   **Backend (Spring Boot):** Exposes a RESTful API, handles business logic, and maps HTTP verbs (`GET`, `POST`, `PUT`, `DELETE`) to database operations.
    
*   **Database (H2 Database):** An embedded, in-memory SQL database that requires zero installation or external database servers, making local development and testing fast and effortless.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/6b1cf866-ac57-4107-89fb-7d5802315148.png align="center")

### Prerequisites

Before starting, ensure you have the following installed on your machine:

*   **Java Development Kit (JDK 17 or 21)**
    
*   **Node.js** (LTS version) & **npm**
    
*   **Angular CLI 18** (Install globally via `npm install -g @angular/cli@18`)
    
*   **IDE / Code Editor:** You can use any editor you prefer. For this guide, we will use **Eclipse** for the Spring Boot backend and **VS Code** for the Angular frontend.
    
*   **API Testing Tool:** You can use either of the API clients like [Hoppscotch](https://hoppscotch.io) or Postman to test backend endpoints. In this guide, we will use [Hoppscotch](https://hoppscotch.io).
    

## 1\. Setting Up the Spring Boot Backend

We will start by generating our backend skeleton using [Spring Initializr](https://start.spring.io/).

### Project Configuration

*   **Project:** Maven
    
*   **Language:** Java
    
*   **Spring Boot:** 4.x (latest stable)
    
*   **Group:** [`com.dev`](http://com.dev)
    
*   **Artifact:** `bookmark-manager-api`
    
*   **Packaging:** Jar
    
*   **Configuration:** Properties
    
*   **Java:** 17 or 21
    

### Dependencies

Add the following three core dependencies:

1.  **Spring Web:** Provides RESTful endpoints and uses Apache Tomcat as the default embedded container.
    
2.  **Spring Data JPA:** Simplifies data access using Hibernate and the Java Persistence API.
    
3.  **H2 Database:** A lightweight, in-memory SQL database for fast local development without configuring an external database server.
    

![Spring Initializr Project Configuration](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/67323761-223c-4ae6-a922-da786ef3e29e.png align="center")

Generate the project, extract the zip file, and import it into **Eclipse** as an *Existing Maven Project*.

### Database & Application Configuration

Open `src/main/resources/`[`application.properties`](http://application.properties) and add the configuration for the H2 database and the embedded web console:

```java
spring.application.name=bookmarkmanager

# Server Port
server.port=8080

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:bookmarkdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=rohit
spring.datasource.password=********

# H2 Console Configuration (for testing)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA / Hibernate Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
```

### Key Configurations Explained:

*   `jdbc:h2:mem:bookmarkdb`: Instructs Spring Boot to run an in-memory database named `bookmarkdb`. The data persists in RAM as long as the application runs.
    
*   `spring.h2.console.enabled=true`: Enables a browser-based GUI to inspect tables and execute SQL queries directly.
    
*   `spring.jpa.hibernate.ddl-auto=update`: Automatically creates or updates the database tables matching our Java entities on application startup.
    
*   [`spring.jpa.show`](http://spring.jpa.show)`-sql=true`: Logs all generated SQL queries directly to the console for easy debugging.
    

You can verify the database setup by running the application and visiting [`http://localhost:8080/h2-console`](http://localhost:8080/h2-console) in your browser. Set the JDBC URL to `jdbc:h2:mem:bookmarkdb`, enter your username and password and click **Connect**.

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/80ac2f73-b71c-45df-8674-7f0e7ff15fe7.png align="center")

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/33f70186-e90e-4e1f-98d9-a42b193f5111.png align="center")

## 2\. Building the Spring Boot Backend

To build a clean, maintainable backend, we follow a layered architecture. Each layer has a single responsibility, ensuring that incoming HTTP requests flow systematically down to the database and back.

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/44941127-0659-49c6-a640-c745e3d0cf02.png align="center")

### The Domain Model ([`Bookmark.java`](http://Bookmark.java))

The entity represents the schema of our table in the database.

*   **File:** `src/main/java/com/dev/bookmarkmanager/model/`[`Bookmark.java`](http://Bookmark.java)
    

```java
package com.dev.bookmarkmanager.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;

@Entity
@Table(name = "bookmarks")
public class Bookmark {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String url;
    private LocalDateTime createdAt;

    public Bookmark() {
        this.createdAt = LocalDateTime.now();
    }

    public Bookmark(String title, String url) {
        this.title = title;
        this.url = url;
        this.createdAt = LocalDateTime.now();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
}
```

**Technical Details:**

*   `@Entity` informs JPA that this class maps to a database table.
    
*   `@Table(name = "bookmarks")` explicitly names the database table.
    
*   `@GeneratedValue(strategy = GenerationType.IDENTITY)` configures the primary key to use an auto-increment column managed by the database.
    
*   The no-argument constructor initializes `createdAt` with the current timestamp automatically whenever a new bookmark instance is created.
    

### The Data Access Layer ([`BookmarkRepository.java`](http://BookmarkRepository.java))

The repository layer abstracts database queries without requiring boilerplate SQL.

*   **File:** `src/main/java/com/dev/bookmarkmanager/repository/`[`BookmarkRepository.java`](http://BookmarkRepository.java)
    

```java
package com.dev.bookmarkmanager.repository;

import com.dev.bookmarkmanager.model.Bookmark;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
}
```

**Technical Details:**

*   By extending `JpaRepository<Bookmark, Long>`, Spring Data JPA automatically provides ready-to-use CRUD operations (`save`, `findById`, `findAll`, `deleteById`) at runtime.
    
*   No implementation class or custom SQL is required for standard operations.
    

### The Business Logic Layer ([`BookmarkService.java`](http://BookmarkService.java))

The service layer contains business validation and orchestrates operations between controllers and repositories.

*   **File:** `src/main/java/com/dev/bookmarkmanager/service/`[`BookmarkService.java`](http://BookmarkService.java)
    

```java
package com.dev.bookmarkmanager.service;

import com.dev.bookmarkmanager.model.Bookmark;
import com.dev.bookmarkmanager.repository.BookmarkRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookmarkService {

    private final BookmarkRepository bookmarkRepository;

    public BookmarkService(BookmarkRepository bookmarkRepository) {
        this.bookmarkRepository = bookmarkRepository;
    }

    public List<Bookmark> getAllBookmarks() {
        return bookmarkRepository.findAll();
    }

    public Optional<Bookmark> getBookmarkById(Long id) {
        return bookmarkRepository.findById(id);
    }

    public Bookmark createBookmark(Bookmark bookmark) {
        return bookmarkRepository.save(bookmark);
    }

    public Bookmark updateBookmark(Long id, Bookmark updatedBookmark) {
        return bookmarkRepository.findById(id).map(existingBookmark -> {
            existingBookmark.setTitle(updatedBookmark.getTitle());
            existingBookmark.setUrl(updatedBookmark.getUrl());
            return bookmarkRepository.save(existingBookmark);
        }).orElseThrow(() -> new RuntimeException("Bookmark not found with id " + id));
    }

    public void deleteBookmark(Long id) {
        bookmarkRepository.deleteById(id);
    }
}
```

**Technical Details:**

*   **Constructor Injection:** The `BookmarkRepository` dependency is injected through the constructor, making the service immutable and easy to unit test.
    
*   **Update Logic:** In `updateBookmark()`, we fetch the existing entity using `findById()`, update only mutable fields (`title`, `url`), and save it back while preserving the original `id` and `createdAt` timestamp.
    

### The REST Controller & CORS ([`BookmarkController.java`](http://BookmarkController.java))

The controller exposes HTTP endpoints, maps incoming request bodies from JSON to Java objects, and returns standard HTTP status codes.

*   **File:** `src/main/java/com/dev/bookmarkmanager/controller/`[`BookmarkController.java`](http://BookmarkController.java)
    

```java
package com.dev.bookmarkmanager.controller;

import com.dev.bookmarkmanager.model.Bookmark;
import com.dev.bookmarkmanager.service.BookmarkService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/bookmarks")
@CrossOrigin(origins = "*")
public class BookmarkController {

    private final BookmarkService bookmarkService;

    public BookmarkController(BookmarkService bookmarkService) {
        this.bookmarkService = bookmarkService;
    }

    @GetMapping
    public List<Bookmark> getAllBookmarks() {
        return bookmarkService.getAllBookmarks();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Bookmark> getBookmarkById(@PathVariable Long id) {
        return bookmarkService.getBookmarkById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Bookmark> createBookmark(@RequestBody Bookmark bookmark) {
        Bookmark created = bookmarkService.createBookmark(bookmark);
        return new ResponseEntity<>(created, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Bookmark> updateBookmark(@PathVariable Long id, @RequestBody Bookmark bookmark) {
        try {
            Bookmark updated = bookmarkService.updateBookmark(id, bookmark);
            return ResponseEntity.ok(updated);
        } catch (RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBookmark(@PathVariable Long id) {
        bookmarkService.deleteBookmark(id);
        return ResponseEntity.noContent().build();
    }
}
```

**Technical Details:**

*   `@RestController`: Combines `@Controller` and `@ResponseBody`, ensuring response values are automatically serialized into JSON.
    
*   `@RequestMapping("/api/bookmarks")`: Sets the base URL path for all endpoints in this controller.
    
*   `@CrossOrigin(origins = "*")`: Configures Cross-Origin Resource Sharing (CORS). We set this to `*` during local development so that both our browser-based API client (such as web-based Hoppscotch, which sends requests from [`https://hoppscotch.io`](https://hoppscotch.io)) and the Angular app ([`http://localhost:4200`](http://localhost:4200)) can communicate with our API without browser security blocks. *(In a production environment, this should always be restricted strictly to your trusted frontend domain).*
    
*   **Status Codes:**
    
    *   `200 OK` for successful fetches and updates.
        
    *   `201 CREATED` when a new bookmark is created via `POST`.
        
    *   `204 NO CONTENT` for successful deletions.
        
    *   `404 NOT FOUND` when an invalid `id` is queried.
        

## 3\. Testing the REST Endpoints & Verifying H2

Before moving to the frontend, it is best practice to verify all endpoints independently using an API testing client like [Hoppscotch](https://hoppscotch.io/) or Postman.

Here is the quick reference table of our REST contract:

<table style="min-width: 100px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>HTTP Method</strong></p></td><td colspan="1" rowspan="1"><p><strong>Endpoint</strong></p></td><td colspan="1" rowspan="1"><p><strong>Description</strong></p></td><td colspan="1" rowspan="1"><p><strong>Expected Status</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>POST</code></p></td><td colspan="1" rowspan="1"><p><code>/api/bookmarks</code></p></td><td colspan="1" rowspan="1"><p>Creates a new bookmark</p></td><td colspan="1" rowspan="1"><p><code>201 Created</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>GET</code></p></td><td colspan="1" rowspan="1"><p><code>/api/bookmarks</code></p></td><td colspan="1" rowspan="1"><p>Fetches all bookmarks</p></td><td colspan="1" rowspan="1"><p><code>200 OK</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>GET</code></p></td><td colspan="1" rowspan="1"><p><code>/api/bookmarks/{id}</code></p></td><td colspan="1" rowspan="1"><p>Fetches a single bookmark by ID</p></td><td colspan="1" rowspan="1"><p><code>200 OK</code> (or <code>404 Not Found</code>)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>PUT</code></p></td><td colspan="1" rowspan="1"><p><code>/api/bookmarks/{id}</code></p></td><td colspan="1" rowspan="1"><p>Updates an existing bookmark</p></td><td colspan="1" rowspan="1"><p><code>200 OK</code> (or <code>404 Not Found</code>)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DELETE</code></p></td><td colspan="1" rowspan="1"><p><code>/api/bookmarks/{id}</code></p></td><td colspan="1" rowspan="1"><p>Deletes a bookmark</p></td><td colspan="1" rowspan="1"><p><code>204 No Content</code></p></td></tr></tbody></table>

### Verifying the CRUD Operations

#### 1\. Create a Bookmark (`POST /api/bookmarks`)

Send a `POST` request with the JSON payload:

```json
{
  "title": "Spring Initializr",
  "url": "https://start.spring.io"
}
```

*   **Response:** Returns `201 Created` along with the persisted object containing the generated `id` and `createdAt` timestamp.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/ee92f1b1-18af-45a7-a733-35b8756e2c68.png align="center")

#### 2\. Retrieve All Bookmarks (`GET /api/bookmarks`)

Send a `GET` request.

*   **Response:** Returns `200 OK` with an array of all saved bookmarks.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/1c8b825d-b11d-434a-bfc1-47e2cd98bf33.png align="center")

#### 3\. Retrieve a specific bookmark (`GET /api/bookmarks/{id}`)

Send a `GET` request.

*   **Response:** Returns `200 OK` with the specific bookmark object.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/ff89a338-024c-41b3-aaae-acf122a81617.png align="center")

#### 4\. Update a Bookmark (`PUT /api/bookmarks/{id}`)

Send a `PUT` request with updated values:

```json
{
  "title": "Spring Initializr (Official)",
  "url": "https://start.spring.io"
}
```

*   **Response:** Returns `200 OK` with the modified fields.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/8c1b1b9a-557a-4b5c-ba08-6a5c927118cf.png align="center")

#### 5\. Delete a Bookmark (`DELETE /api/bookmarks/{id}`)

Send a `DELETE` request for a specific ID.

*   **Response:** Returns `204 No Content`, indicating the resource was deleted successfully with no response body needed.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/8e215ee1-b6b2-4f05-af35-160471c6a08c.png align="center")

### Inspecting the H2 In-Memory Database

You can also verify that Hibernate created the schema and persisted your records directly inside the database:

1.  Open your browser and navigate to [`http://localhost:8080/h2-console`](http://localhost:8080/h2-console).
    
2.  Connect using JDBC URL: `jdbc:h2:mem:bookmarkdb` and your own username and password.
    
3.  In the left panel, you will see the auto-generated `BOOKMARKS` table.
    
4.  Run the query `SELECT * FROM BOOKMARKS;` to view all saved records.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/095bda54-5a2a-445d-98ce-4b706d26dcb9.png align="center")

## 4\. Setting Up the Modern Angular Frontend

With our Spring Boot API running and tested, we can now build the user interface using Angular. We will create a clean client using modern **standalone components** and standard client-side routing.

### Step 1: Initialize the Project

Generate a new Angular application using the Angular CLI:

```shell
ng new bookmark-manager-ui
```

During initialization, select the following prompts:

*   **Which stylesheet format would you like to use?** `CSS`
    
*   **Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?** `N` (No)
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/4b207adc-e306-4950-8391-5edbfc696ff3.png align="center")

Navigate into the project directory and open it in **VS Code**:

```shell
cd bookmark-manager-ui
code .
```

### Step 2: Configure the Global HTTP Client (`app.config.ts`)

In modern standalone Angular applications, dependency injection providers are configured globally inside `src/app/app.config.ts`. We need to register `provideHttpClient()` so our application can send HTTP requests to our Spring Boot backend.

*   **File:** `src/app/app.config.ts`
    

```typescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
};
```

**Technical Details:**

*   `ApplicationConfig`: The TypeScript interface that defines the configuration object passed to `bootstrapApplication()` in `main.ts`.
    
*   `provideZoneChangeDetection({ eventCoalescing: true })`: Configures Zone.js change detection. Setting `eventCoalescing: true` batches multiple synchronous micro-events into a single change detection cycle, reducing unnecessary UI re-renders and boosting performance.
    
*   `provideRouter(routes)`: Registers the client-side router and binds the defined route table (`app.routes.ts`) to manage browser URL navigation without full page reloads.
    
*   `provideHttpClient()`: Injects Angular's fetch-capable HTTP client globally into the root dependency injection container, enabling services to dispatch asynchronous HTTP requests.
    

### Step 3: Scaffold Core Folders & Files

To keep our frontend clean and modular, we will generate the required model, service, and components using the Angular CLI.

Run the following commands in your terminal:

```shell
# 1. Generate the Service
ng g s services/bookmark

# 2. Generate the Bookmark List Component
ng g c components/bookmark-list

# 3. Generate the Unified Bookmark Form Component
ng g c components/bookmark-form
```

Next, create the TypeScript interface file manually:

*   Create `src/app/models/bookmark.model.ts`
    

### Understanding the Generated Structure & `.spec.ts` Files

After scaffolding, your `src/app/` folder will be structured as follows:

```plaintext
src/app
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.config.ts
├── app.routes.ts
├── components
│   ├── bookmark-form
│   │   ├── bookmark-form.component.css
│   │   ├── bookmark-form.component.html
│   │   ├── bookmark-form.component.spec.ts
│   │   └── bookmark-form.component.ts
│   └── bookmark-list
│       ├── bookmark-list.component.css
│       ├── bookmark-list.component.html
│       ├── bookmark-list.component.spec.ts
│       └── bookmark-list.component.ts
├── models
│   └── bookmark.model.ts
└── services
    ├── bookmark.service.spec.ts
    └── bookmark.service.ts
```

> **A Note on** `*.spec.ts` **Files:**
> 
> When the Angular CLI generates components and services, it automatically creates companion `*.spec.ts` files. These are **unit test suites** intended for automated testing frameworks (such as Jasmine/Karma). They are not required to build or run the application, so you can safely ignore or remove them since we will not be writing any automated tests for this project in this guide.

## 5\. Data Model, API Service & Routing

With the initial setup complete, we now connect the frontend to our Spring Boot REST API using TypeScript interfaces, an Angular Service, and client-side routing.

### 1\. Data Model (`bookmark.model.ts`)

Define a TypeScript interface to enforce type safety across the frontend that mirrors our backend `Bookmark` entity.

*   **File:** `src/app/models/bookmark.model.ts`
    

```typescript
export interface Bookmark {
  id?: number;
  title: string;
  url: string;
  createdAt?: string;
}
```

**Technical Details:**

*   `id?: number` & `createdAt?: string`: Marked as optional (`?`) because they are generated automatically by the backend/database and will not exist prior to saving a new bookmark.
    
*   `title: string` & `url: string`: Required fields for both UI inputs and backend payload validation.
    

### 2\. API Service Layer (`bookmark.service.ts`)

The service layer encapsulates all HTTP network communication, keeping component classes clean and focused solely on UI view logic.

*   **File:** `src/app/services/bookmark.service.ts`
    

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Bookmark } from '../models/bookmark.model';

@Injectable({
  providedIn: 'root'
})
export class BookmarkService {
  private readonly apiUrl = 'http://localhost:8080/api/bookmarks';

  constructor(private http: HttpClient) {}

  getBookmarks(): Observable<Bookmark[]> {
    return this.http.get<Bookmark[]>(this.apiUrl);
  }

  getBookmarkById(id: number): Observable<Bookmark> {
    return this.http.get<Bookmark>(`${this.apiUrl}/${id}`);
  }

  createBookmark(bookmark: Bookmark): Observable<Bookmark> {
    return this.http.post<Bookmark>(this.apiUrl, bookmark);
  }

  updateBookmark(id: number, bookmark: Bookmark): Observable<Bookmark> {
    return this.http.put<Bookmark>(`${this.apiUrl}/${id}`, bookmark);
  }

  deleteBookmark(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}
```

**Technical Details:**

*   `@Injectable({ providedIn: 'root' })`: Registers this service as a singleton in the root injector, ensuring a single shared instance across the entire application lifecycle.
    
*   **HTTP Client Abstraction:** Injects `HttpClient` to manage network calls to the base endpoint ([`http://localhost:8080/api/bookmarks`](http://localhost:8080/api/bookmarks)), automatically serializing JavaScript objects into JSON payloads on outgoing requests and deserializing backend responses into typed TypeScript models.
    
*   **CRUD API Operations:**
    
    *   `getBookmarks()`: Dispatches an HTTP `GET` to fetch the full array of bookmarks.
        
    *   `getBookmarkById(id)`: Dispatches an HTTP `GET` to retrieve a specific bookmark by appending its ID to the URL path.
        
    *   `createBookmark(bookmark)`: Dispatches an HTTP `POST` sending the new bookmark object in the request body.
        
    *   `updateBookmark(id, bookmark)`: Dispatches an HTTP `PUT` to `/api/bookmarks/{id}` carrying the updated fields.
        
    *   `deleteBookmark(id)`: Dispatches an HTTP `DELETE` to `/api/bookmarks/{id}` to remove the resource.
        
*   **RxJS Observables:** Every method returns an `Observable<T>`, providing an asynchronous stream that components can subscribe to for handling response data, loading states, and error propagation.
    

### 3\. Application Routing (`app.routes.ts`)

Rather than showing all operations on a single cluttered screen, we map distinct URL paths to dedicated views.

*   **File:** `src/app/app.routes.ts`
    

```typescript
import { Routes } from '@angular/router';
import { BookmarkListComponent } from './components/bookmark-list/bookmark-list.component';
import { BookmarkFormComponent } from './components/bookmark-form/bookmark-form.component';

export const routes: Routes = [
  { path: '', component: BookmarkListComponent },
  { path: 'add-bookmark', component: BookmarkFormComponent },
  { path: 'update-bookmark/:id', component: BookmarkFormComponent },
  { path: '**', redirectTo: '' }
];
```

**Technical Details:**

*   `{ path: '', component: BookmarkListComponent }`: Default root path that renders the list of all bookmarks.
    
*   `{ path: 'add-bookmark', component: BookmarkFormComponent }`: Route for creating new entries.
    
*   `{ path: 'update-bookmark/:id', component: BookmarkFormComponent }`: Route for updating an entry, capturing the entity identifier via the dynamic `:id` route parameter.
    
*   `{ path: '**', redirectTo: '' }`: Wildcard fallback route that redirects any undefined URLs back to the home list view.
    

## 6\. Building the UI Components

With routing and services in place, we implement the interactive user interface using standalone components, modern Angular control flow (`@if`, `@for`), and a unified form architecture.

### 1\. The Bookmark List Component

The list component fetches all bookmarks on initialization, renders them cleanly, and handles user navigation for editing or triggering deletions.

*   **TypeScript:** `src/app/components/bookmark-list/bookmark-list.component.ts`
    

```typescript
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
import { BookmarkService } from '../../services/bookmark.service';
import { Bookmark } from '../../models/bookmark.model';

@Component({
  selector: 'app-bookmark-list',
  standalone: true,
  imports: [CommonModule, RouterModule],
  templateUrl: './bookmark-list.component.html',
  styleUrls: ['./bookmark-list.component.css']
})
export class BookmarkListComponent implements OnInit {
  bookmarks: Bookmark[] = [];

  constructor(
    private bookmarkService: BookmarkService,
    private router: Router
  ) {}

  ngOnInit(): void {
    this.loadBookmarks();
  }

  loadBookmarks(): void {
    this.bookmarkService.getBookmarks().subscribe({
      next: (data) => (this.bookmarks = data),
      error: (err) => console.error('Error fetching bookmarks', err)
    });
  }

  editBookmark(id: number | undefined): void {
    if (id) {
      this.router.navigate(['/update-bookmark', id]);
    }
  }

  deleteBookmark(id: number | undefined): void {
    if (!id) return;
    if (confirm('Are you sure you want to delete this bookmark?')) {
      this.bookmarkService.deleteBookmark(id).subscribe({
        next: () => this.loadBookmarks(),
        error: (err) => console.error('Error deleting bookmark', err)
      });
    }
  }
}
```

*   **Template:** `src/app/components/bookmark-list/bookmark-list.component.html`
    

```html
<section class="card">
    <div class="list-header">
        <h2>All Bookmarks ({{ bookmarks.length }})</h2>
        <a routerLink="/add-bookmark" class="btn btn-primary">+ Add New Bookmark</a>
    </div>

    @if (bookmarks.length === 0) {
    <div class="empty-state">
        <p>No bookmarks found. Click the button above to add one!</p>
    </div>
    } @else {
    <ul class="bookmark-list">
        @for (item of bookmarks; track item.id) {
        <li class="bookmark-item">
            <div class="bookmark-info">
                <h3>{{ item.title }}</h3>
                <a [href]="item.url" target="_blank" rel="noopener noreferrer">
                    {{ item.url }}
                </a>
                @if (item.createdAt) {
                <small>Added: {{ item.createdAt | date:'medium' }}</small>
                }
            </div>

            <div class="item-actions">
                <button class="btn btn-edit" (click)="editBookmark(item.id)">Edit</button>
                <button class="btn btn-delete" (click)="deleteBookmark(item.id)">Delete</button>
            </div>
        </li>
        }
    </ul>
    }
</section>
```

*   **Styles:** `src/app/components/bookmark-list/bookmark-list.component.css`
    

```css
.card {
    background: #ffffff;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.list-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1.25rem;
}

.list-header h2 {
    margin: 0;
    font-size: 1.25rem;
    color: #111827;
}

.bookmark-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.bookmark-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 0;
    border-bottom: 1px solid #f3f4f6;
}

.bookmark-item:last-child {
    border-bottom: none;
}

.bookmark-info h3 {
    margin: 0 0 0.25rem 0;
    font-size: 1.05rem;
    color: #111827;
}

.bookmark-info a {
    color: #2563eb;
    text-decoration: none;
    font-size: 0.875rem;
    display: block;
}

.bookmark-info a:hover {
    text-decoration: underline;
}

.bookmark-info small {
    color: #9ca3af;
    font-size: 0.75rem;
}

.item-actions {
    display: flex;
    gap: 0.5rem;
}

.empty-state {
    text-align: center;
    color: #6b7280;
    padding: 2rem 0;
}

.btn {
    padding: 0.55rem 0.95rem;
    border: none;
    border-radius: 6px;
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}

.btn-primary {
    background-color: #2563eb;
    color: #ffffff;
}

.btn-primary:hover {
    background-color: #1d4ed8;
}

.btn-edit {
    background-color: #f3f4f6;
    color: #1f2937;
    border: 1px solid #e5e7eb;
}

.btn-edit:hover {
    background-color: #e5e7eb;
}

.btn-delete {
    background-color: #fee2e2;
    color: #dc2626;
}

.btn-delete:hover {
    background-color: #fecaca;
}
```

**Technical Details:**

*   **Component State Management:** `bookmarks: Bookmark[] = []` acts as the local component state. Inside `ngOnInit()`, the `loadBookmarks()` method subscribes to `bookmarkService.getBookmarks()` and assigns the incoming array directly to `this.bookmarks`, triggering Angular's change detection to update the template.
    
*   **Modern Control Flow (**`@if` **/** `@else` **&** `@for`**):**
    
    *   Evaluates state natively without needing legacy directives like `*ngIf` or `*ngFor`.
        
    *   `@if (bookmarks.length === 0)` displays a user-friendly empty state when no bookmarks exist.
        
    *   `@for (item of bookmarks; track` [`item.id`](http://item.id)`)` iterates over the list and enforces item identity tracking via `track` [`item.id`](http://item.id) for efficient DOM updates during additions, edits, or removals.
        
*   **Action Handlers:**
    
    *   `editBookmark(id)`: Uses Angular's `Router` (`this.router.navigate(['/update-bookmark', id])`) to navigate imperatively to the update view carrying the target bookmark's ID.
        
    *   `deleteBookmark(id)`: Displays a confirmation prompt, calls `bookmarkService.deleteBookmark(id)`, and upon successful deletion, re-invokes `this.loadBookmarks()` to fetch the updated state from the backend
        

### 2\. The Unified Form Component

Rather than duplicating logic across separate "Add" and "Edit" views, a single `BookmarkFormComponent` dynamically handles both operations based on URL parameters.

*   **TypeScript:** `src/app/components/bookmark-form/bookmark-form.component.ts`
    

```typescript
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { BookmarkService } from '../../services/bookmark.service';
import { Bookmark } from '../../models/bookmark.model';

@Component({
  selector: 'app-bookmark-form',
  standalone: true,
  imports: [CommonModule, FormsModule, RouterModule],
  templateUrl: './bookmark-form.component.html',
  styleUrls: ['./bookmark-form.component.css']
})
export class BookmarkFormComponent implements OnInit {
  id?: number;
  isEditMode: boolean = false;

  bookmark: Bookmark = {
    title: '',
    url: ''
  };

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private bookmarkService: BookmarkService
  ) {}

  ngOnInit(): void {
    const routeId = this.route.snapshot.paramMap.get('id');
    if (routeId) {
      this.id = Number(routeId);
      this.isEditMode = true;
      this.bookmarkService.getBookmarkById(this.id).subscribe({
        next: (data) => (this.bookmark = data),
        error: (err) => {
          console.error('Error fetching bookmark for edit:', err);
          this.router.navigate(['/']);
        }
      });
    }
  }

  saveBookmark(): void {
    if (!this.bookmark.title.trim() || !this.bookmark.url.trim()) {
      alert('Please provide both Title and URL');
      return;
    }

    if (this.isEditMode && this.id) {
      this.bookmarkService.updateBookmark(this.id, this.bookmark).subscribe({
        next: () => this.router.navigate(['/']),
        error: (err) => console.error('Error updating bookmark:', err)
      });
    } else {
      this.bookmarkService.createBookmark(this.bookmark).subscribe({
        next: () => this.router.navigate(['/']),
        error: (err) => console.error('Error creating bookmark:', err)
      });
    }
  }
}
```

*   **Template:** `src/app/components/bookmark-form/bookmark-form.component.html`
    

```html
<section class="card">
    <h2>{{ isEditMode ? 'Edit Bookmark' : 'Add New Bookmark' }}</h2>

    <form (ngSubmit)="saveBookmark()">
        <div class="form-group">
            <label for="title">Title</label>
            <input id="title" type="text" name="title" placeholder="e.g. Spring Initializr" [(ngModel)]="bookmark.title"
                required />
        </div>

        <div class="form-group">
            <label for="url">URL</label>
            <input id="url" type="url" name="url" placeholder="e.g. https://start.spring.io" [(ngModel)]="bookmark.url"
                required />
        </div>

        <div class="form-actions">
            <button type="submit" class="btn btn-primary">
                {{ isEditMode ? 'Update Bookmark' : 'Save Bookmark' }}
            </button>
            <a routerLink="/" class="btn btn-secondary">Cancel</a>
        </div>
    </form>
</section>
```

*   **Styles:** `src/app/components/bookmark-form/bookmark-form.component.css`
    

```css
.card {
    background: #ffffff;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.card h2 {
    margin-top: 0;
    margin-bottom: 1.25rem;
    font-size: 1.25rem;
    color: #111827;
}

.form-group {
    margin-bottom: 1rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.35rem;
    font-weight: 500;
    font-size: 0.875rem;
}

.form-group input {
    width: 100%;
    padding: 0.65rem 0.75rem;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-size: 0.95rem;
    box-sizing: border-box;
}

.form-group input:focus {
    outline: none;
    border-color: #2563eb;
    box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}

.form-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 1.25rem;
}

.btn {
    padding: 0.6rem 1rem;
    border: none;
    border-radius: 6px;
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}

.btn-primary {
    background-color: #2563eb;
    color: #ffffff;
}

.btn-primary:hover {
    background-color: #1d4ed8;
}

.btn-secondary {
    background-color: #e5e7eb;
    color: #374151;
}

.btn-secondary:hover {
    background-color: #d1d5db;
}
```

**Technical Details:**

*   **Dynamic Mode Detection:** On `ngOnInit()`, the component checks `this.route.snapshot.paramMap.get('id')`:
    
    *   **Edit Mode:** If an `:id` exists in the route, it sets `isEditMode = true` and invokes `getBookmarkById(id)` to pre-populate the input fields.
        
    *   **Create Mode:** If no ID parameter exists, it initializes with a blank model.
        
*   **Two-Way Data Binding (**`[(ngModel)]`**):** Synchronizes user keystrokes in `<input>` elements directly with the `bookmark` model object.
    
*   **Dynamic UI & Submission Routing:**
    
    *   The template dynamically updates headers and button labels (`"Save Bookmark"` vs `"Update Bookmark"`) using ternary expressions (`{{ isEditMode ? ... : ... }}`).
        
    *   The `saveBookmark()` method checks `isEditMode` to route the submission to either `service.updateBookmark()` or `service.createBookmark()`, navigating back to `/` upon success via `this.router.navigate(['/'])`.
        

### 3\. Root Application Shell

The root component serves as the application shell, providing the global header and rendering active route components dynamically via `<router-outlet>`.

*   **File:** `src/app/app.component.ts`
    

```typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'bookmarkmanager-ui';
}
```

*   **File:** `src/app/app.component.html`
    

```html
<main class="container">
  <header class="header">
    <h1>Bookmark Manager</h1>
    <p>A full-stack CRUD application powered by Angular, Spring Boot & H2</p>
  </header>

  <router-outlet></router-outlet>
</main>
```

*   **File:** `src/app/app.component.css`
    

```css
:host {
    display: block;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    color: #1f2937;
    background-color: #f8fafc;
    min-height: 100vh;
    padding: 2rem 1rem;
}

.container {
    max-width: 760px;
    margin: 0 auto;
}

.header {
    text-align: center;
    margin-bottom: 2rem;
}

.header h1 {
    margin: 0 0 0.5rem 0;
    font-size: 2rem;
    color: #111827;
}

.header p {
    margin: 0;
    color: #6b7280;
    font-size: 0.95rem;
}
```

## 7\. End-to-End Execution & Verification

With both the backend and frontend fully wired up, you can now run the complete full-stack workflow.

### Running the Complete Stack

1.  **Start the Backend:** In **Eclipse**, locate your main entry class [`BookmarkManagerApplication.java`](http://BookmarkManagerApplication.java) under `src/main/java/com/dev/bookmarkmanager/`, right-click on it, and select **Run As --> Java Application**. The backend embedded Tomcat server will start up on port `8080`.
    
2.  **Start the Frontend:** Open your terminal in the `bookmark-manager-ui` project directory and run `ng serve`. Open your browser and navigate to [`http://localhost:4200`](http://localhost:4200).
    

### Verifying the Workflow

1.  **Viewing the Empty State:** Navigate to [`http://localhost:4200`](http://localhost:4200). On the initial launch before any records are created, the application detects an empty array and renders a clean empty state prompt.
    
    ![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/2c756bd9-b8d7-4030-ba48-1814383a109f.png align="center")
    
2.  **Adding a New Bookmark:** Click the **"+ Add New Bookmark"** button to navigate to `/add-bookmark`. Enter the title and URL into the input fields and click **"Save Bookmark"**. The application dispatches an HTTP `POST` request to the Spring Boot backend and redirects back to the main list.
    
    ![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/1937c1ae-2824-493b-b4ee-50d1bbf435da.png align="center")
    
3.  **Viewing the Populated Bookmark List:** Once entries are saved, the list view dynamically displays each bookmark card complete with title, clickable external URL, creation timestamp, and dedicated action buttons.
    
    ![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/d49c7313-1d4f-4d5b-bc43-4262e7809135.png align="center")
    
4.  **Updating an Existing Bookmark:** Click the **"Edit"** button on any bookmark card. The router navigates to `/update-bookmark/:id`, where the unified form detects the route parameter, fetches the current record, and pre-populates the inputs. Modify the details and click **"Update Bookmark"** to send the `PUT` request.
    
    ![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/a38c0125-e4f4-4ab8-bdff-a54f733e9828.png align="center")
    
    ![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/52647607-fdfd-4652-9572-ed8973427a02.png align="center")
    
5.  **Deleting a Bookmark:** Click the **"Delete"** button next to an item and confirm the prompt. An HTTP `DELETE` request is sent to remove the entity, and the UI immediately triggers a refresh to reflect the deletion.
    

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/5208769d-1465-4b06-8e25-49662d19b0dc.png align="center")

![](https://cdn.hashnode.com/uploads/covers/694ad25685b21dc282ce6bc6/34e1cbaf-5b40-40bb-8116-5c142166f725.png align="center")

1.  **Inspecting Database Persistence (H2 Console):** At any point while adding, updating, or deleting records in the UI, you can open [`http://localhost:8080/h2-console`](http://localhost:8080/h2-console) in your browser. Connect using the JDBC URL `jdbc:h2:mem:bookmarkdb` and run `SELECT * FROM BOOKMARKS;` to watch your database table reflect every frontend state change in real time.
    

## Summary

In this guide, we built a fully decoupled full-stack application using modern standards:

*   A **Spring Boot** REST API layered with JPA entities, repositories, services, and controllers.
    
*   An embedded **H2 database** with active console inspection for zero-config persistence.
    
*   An **Angular 18** client leveraging standalone components, modern control flow blocks (`@if`, `@for`), typed HTTP services, and a unified form component.
