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
Benefit | Explanation |
---|---|
Maintainability | Business logic centralized in services. |
Testability | Easy to unit test services without UI dependency. |
Code reuse | Single source of truth across platforms. |
UI Flexibility | Change 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.