Building a Headless Angular App for Mobile and Desktop

June 1, 2025

As apps grow complex and target multiple platforms—Progressive Web Apps (PWAs), native mobile, and desktop wrappers—it becomes essential to decouple UI from business logic. This approach is often called the headless pattern, where the core application logic and state are isolated and reusable across different frontends.

In this post, we’ll explore how to build a headless Angular app that can power both mobile and desktop interfaces, improving maintainability, testability, and code reuse.


Why Headless Architecture?

  • Separation of concerns: UI components focus purely on presentation.
  • Reusability: Business logic can be reused in native apps (Ionic, Capacitor) or desktop (Electron) apps.
  • Easier testing: Logic can be tested independently without UI dependencies.
  • Flexibility: Swap or upgrade UI frameworks without rewriting core logic.

Step 1: Designing the Headless Service Layer

Create Angular services that encapsulate all your state, API calls, and business rules.

import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable({ providedIn: 'root' })
export class TodoService {
  private todos = signal<Todo[]>([]);

  readonly todos$ = this.todos.asReadonly();

  constructor(private http: HttpClient) {}

  loadTodos() {
    this.http.get<Todo[]>('/api/todos').subscribe(todos => {
      this.todos.set(todos);
    });
  }

  addTodo(title: string) {
    const newTodo: Todo = { id: Date.now(), title, completed: false };
    this.todos.update(current => [...current, newTodo]);
  }

  toggleTodo(id: number) {
    this.todos.update(current => 
      current.map(todo => todo.id === id ? {...todo, completed: !todo.completed} : todo)
    );
  }
}
  • Notice we use Angular signals to hold the state reactively.
  • The service knows nothing about how UI renders this data.

Step 2: Building a Thin UI Layer

Your UI components just consume the service’s reactive state and expose user interactions.

import { Component, OnInit } from '@angular/core';
import { TodoService, Todo } from './todo.service';

@Component({
  selector: 'app-todo-list',
  template: `
    <ul>
      <li *ngFor="let todo of todos()">
        <label>
          <input type="checkbox" [checked]="todo.completed" (change)="toggle(todo.id)" />
          {{ todo.title }}
        </label>
      </li>
    </ul>

    <input [(ngModel)]="newTodoTitle" placeholder="Add new todo" />
    <button (click)="add()">Add</button>
  `
})
export class TodoListComponent implements OnInit {
  todos = this.todoService.todos$;
  newTodoTitle = '';

  constructor(private todoService: TodoService) {}

  ngOnInit() {
    this.todoService.loadTodos();
  }

  toggle(id: number) {
    this.todoService.toggleTodo(id);
  }

  add() {
    if (this.newTodoTitle.trim()) {
      this.todoService.addTodo(this.newTodoTitle.trim());
      this.newTodoTitle = '';
    }
  }
}

Step 3: Reusing Logic for Mobile/Desktop

  • For mobile apps: Use Ionic or Capacitor with Angular and inject the same TodoService. Your native UI components just bind to the same reactive data.
  • For desktop apps: Use Electron with Angular and reuse the services for data and business logic.
  • For PWAs: The web UI is just another consumer of the headless services.

Step 4: Handling Platform Differences

You can abstract platform-specific APIs behind Angular services:

@Injectable({ providedIn: 'root' })
export class PlatformService {
  isMobile() {
    return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
  }

  // Add other platform-specific APIs here
}

Use this service in your UI components to conditionally render or behave differently without touching business logic.


Benefits Summary

BenefitExplanation
MaintainabilityBusiness logic centralized in services.
TestabilityEasy to unit test services without UI dependency.
Code reuseSingle source of truth across platforms.
UI FlexibilityChange UI tech or design independently.

Conclusion

Headless Angular architecture is a powerful way to build scalable, cross-platform apps that share logic while adapting UI per platform. Leveraging Angular 18’s Signals alongside smart service design unlocks this approach elegantly.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *