Skip to content

Retrieve Slice: Loading data without component logic

Loading data sounds harmless.

A GET, a bit of loading, maybe an error state, done.

Until the component suddenly does more than render:

  • it knows HTTP endpoints
  • it distinguishes technical loading states
  • it maps DTOs
  • it builds a model for the template
  • it decides on the empty state
  • it shows error text
  • it eventually calls reload() too

That works.

Until the next flow comes along.

That’s why this article walks through a deliberately structured retrieve slice with a real HTTP endpoint:

https://lorem-api.com/api/article/foo

The slice uses:

  • httpResource in the infrastructure
  • an NgRx Signal Store in +state
  • a thin application facade
  • a presentation layer that only renders signals

Not as a universal truth.

But as a concrete structure a team can discuss.


Retrieve is not a GET. Retrieve is a slice.

The most important point:

httpResource is part of the technical API boundary.

httpResource is not an ordinary UI signal.

Angular describes httpResource as a reactive wrapper around HttpClient. It creates HTTP requests and exposes the response, request status, and errors as signals. Because httpResource sits on top of HttpClient, it also runs through the Angular HTTP stack, including interceptors.

That puts httpResource conceptually closer to the external API than to presentation.

That’s exactly why it belongs in the infrastructure in this slice.

Not because it’s bad or “dirty.”

But because it models external access.

If presentation knows an httpResource, it automatically knows technical details of the API boundary:

  • hasValue()
  • value()
  • isLoading()
  • error()
  • reload()
  • request status
  • parse behavior

That’s API leakage.

The component would no longer just render a ViewModel. It would start interpreting the semantics of an HTTP resource.

That’s why presentation shouldn’t know an httpResource. Neither should the application layer.

And the store shouldn’t have to interpret the concrete HttpResourceRef either.

The infrastructure keeps it private and exposes a small, named signal API:

  • the currently readable article
  • the loading state
  • the error state
  • an explicit reload operation

The store consumes this API.

It knows the source.

But no longer its framework mechanics.


The API delivers an article.

The external response includes, among other things:

  • slug
  • title
  • subtitle
  • image
  • author
  • content

That’s the API model.

The frontend internally works with an entity.

The UI renders a ViewModel.

These are three different things.

API DTO
↓ infrastructure mapper
Entity
↓ state mapper
ViewModel
↓ presentation
Template

This separation is the core of the slice.

The Anti-Corruption Layer article explains why foreign models shouldn’t move through the frontend unfiltered. Here, it’s not about the why again, but about the concrete place in the code.


One possible structure for the slice:

article/
├── entities/
│ └── article.model.ts
├── infrastructure/
│ ├── article.dto.ts
│ ├── article.mapper.ts
│ └── article.resource.ts
├── +state/
│ ├── article.store.ts
│ ├── article.vm.ts
│ └── article-view-model.mapper.ts
├── application/
│ └── article.facade.ts
└── presentation/
├── article-page.component.ts
└── article-page.component.html

The names aren’t magic.

What matters is the direction of responsibility:

presentation → application → +state → infrastructure

The upper layer knows the abstraction beneath it.

But it doesn’t reach across all layers.


1. Infrastructure: httpResource and the DTO boundary

Section titled “1. Infrastructure: httpResource and the DTO boundary”

The infrastructure knows the external API.

It knows:

  • where to load from
  • which DTO is expected
  • where the response gets translated into an internal entity

In a production system, this boundary can additionally perform runtime validation.

For example with Zod, Valibot, or a comparable library.

For this first slice, that’s deliberately left out.

Not because validation isn’t important.

But because this article is meant to show the layered structure.

Runtime validation matters, but it isn’t the focus of this first slice.

export interface ArticleAuthorDto {
readonly id: string;
readonly name: string;
readonly avatar: string;
readonly email: string;
}
export interface ArticleDto {
readonly slug: string;
readonly title: string;
readonly subtitle: string;
readonly image: string;
readonly author: ArticleAuthorDto;
readonly content: string;
}

The DTO doesn’t describe the frontend’s truth.

It only describes the API’s contract.

If the API delivers image, the UI doesn’t necessarily have to use the same field name later.

The domain shape of the slice lives here in entities/.

This deliberately follows the DDD-adjacent Angular terminology that Manfred Steyer, among others, uses. For this article, there’s nothing mystical about it: entities/ contains the internal domain models the slice works with.

The infrastructure knows DTOs.

The rest of the slice works with entities.

export interface ArticleAuthor {
readonly id: string;
readonly name: string;
readonly avatarUrl: string;
readonly email: string;
}
export interface Article {
readonly slug: string;
readonly title: string;
readonly subtitle: string;
readonly heroImageUrl: string;
readonly author: ArticleAuthor;
readonly paragraphs: readonly string[];
}

You can argue about whether this folder should be called entities, model, or domain.

The decisive point isn’t the folder name.

The decisive point is:

The entity is not the DTO.

The infrastructure maps from outside to inside.

Here, deliberately, two things happen:

  1. parseArticleResponse()
  2. toDomain()
import { Article, ArticleAuthor } from '../entities/article.model';
import { ArticleAuthorDto, ArticleDto } from './article.dto';
export const parseArticleResponse = (value: unknown): Article => toDomain(value as ArticleDto);
export const toDomain = ({ slug, title, subtitle, image, author, content }: ArticleDto): Article => ({
slug,
title,
subtitle,
heroImageUrl: image,
author: toAuthorDomain(author),
paragraphs: toParagraphs(content),
});
const toAuthorDomain = ({ id, name, avatar, email }: ArticleAuthorDto): ArticleAuthor => ({
id,
name,
avatarUrl: avatar,
email,
});
const toParagraphs = (content: string): readonly string[] =>
content
.split(/\n+/)
.map((paragraph) => paragraph.trim())
.filter(Boolean);

parseArticleResponse() deliberately uses a type cast here:

value as ArticleDto;

That’s not runtime validation.

The cast just tells the TypeScript compiler:

Treat this value as an ArticleDto from here on.

Whether the API actually delivers this shape at runtime isn’t checked by this.

For this first retrieve slice, that’s intentional. The article is meant to show the layered structure: httpResource, infrastructure mapping, signal store, ViewModel, and presentation.

In a production system, I’d make this boundary more robust — for example with Zod, Valibot, io-ts, or a manual type-guard check.

That would be its own article, though.

Here, the type cast is enough as a deliberately simplified spot where real runtime validation can be added later.

What matters here:

unknown API response → ArticleDto → Article

After this file, the API model is gone.

The rest of the frontend works with Article.

Now comes httpResource.

import { computed, Injectable } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Article } from '../entities/article.model';
import { parseArticleResponse } from './article.mapper';
@Injectable()
export class ArticleResource {
private readonly resource = httpResource<Article>(
() => ({
url: 'https://lorem-api.com/api/article/foo',
method: 'GET',
}),
{
parse: parseArticleResponse,
},
);
readonly article = computed<Article | null>(() => {
if (!this.resource.hasValue()) {
return null;
}
return this.resource.value();
});
readonly isLoading = this.resource.isLoading;
readonly error = this.resource.error;
reload(): void {
this.resource.reload();
}
}

This class is deliberately a class.

Of course you could write the wrapper as a factory function too.

But httpResource needs an Angular injection context and creates a stateful source. The class makes that immediately visible:

  • Angular manages the instance
  • the provider determines its scope
  • the concrete resource stays private
  • only defined signals and operations go out

For this slice, that’s more readable than a factory whose injection context and lifecycle only become clear from the call site.

The underlying Angular resource remains an implementation detail:

private HttpResourceRef
ArticleResource
article | isLoading | error | reload

That’s more than a wrapper around a method.

HttpResourceRef has its own technical semantics:

  • value() cannot be read safely in every state
  • hasValue() protects that access
  • loading and error belong to the asynchronous lifecycle
  • reload() is a capability of the concrete technical source

This behavior is handled exactly once, at the infrastructure boundary.

The store doesn’t have to interpret it again afterward.

The public article signal therefore delivers:

Article | null

null doesn’t mean here:

The API delivered an empty article.

It means:

The technical source currently has no readable article.

As soon as an Article exists, it must be a valid internal model. The infrastructure boundary is responsible for that.

An artificial EMPTY_ARTICLE usually wouldn’t be a better solution for a single model. It would satisfy the TypeScript type, but it would quickly create an entity with an empty ID, invented required values, or invalid invariants.

For collections, an empty array can be a neutral value.

For a single entity, absence is usually more honest than an invented domain model.

reload() also deliberately stays in the infrastructure API:

reload(): void {
this.resource.reload();
}

The wrapper doesn’t invent a domain use case.

It just prevents the store from having to know the concrete HttpResourceRef merely to reload the same source.

The store doesn’t have to know a URL.

It doesn’t have to know how value() gets secured.

Presentation doesn’t have to know a resource.

The application layer doesn’t have to know HTTP details.

httpResource isn’t some global magic passed around the whole frontend here.

It’s the private technical implementation of a clear infrastructure API.


2. +state: orchestrate source signals, derive the ViewModel

Section titled “2. +state: orchestrate source signals, derive the ViewModel”

The store no longer consumes the Angular resource.

It consumes the public API of the infrastructure adapter.

That is the boundary that matters.

A publicly leaked HttpResourceRef would be bad:

readonly articleResource = httpResource<Article>(...);

Then every consuming layer would have to reinterpret its lifecycle.

Better is:

private httpResource
safe infrastructure signals
store orchestration
ViewModel

The store doesn’t expose an httpResource.

And it doesn’t contain a technical case distinction over its state either.

The ViewModel describes what the page actually needs.

export interface ArticleAuthorVm {
readonly name: string;
readonly avatarUrl: string;
readonly emailLabel: string;
}
export interface ArticleVm {
readonly title: string;
readonly subtitle: string;
readonly heroImageUrl: string;
readonly author: ArticleAuthorVm;
readonly paragraphs: readonly string[];
}

This model is UI-oriented.

emailLabel isn’t backend language.

The ViewModel is a deliberate decision made by the UI. The foundational article ViewModel Aggregation describes this idea in more depth. Here, the only interesting question is: where does it come from in the code?

loading and error are deliberately not part of the ViewModel in this slice.

They come from the resource and get provided by the store as separate signals. The ViewModel stays a simple projection here:

Article | null → ArticleVm | null

Of course, it can make sense elsewhere to build more complex page-state models: differentiated error states, partial loading, empty states, retry models, skeleton configurations, or domain-specific loading phases.

That’s not the focus of this first retrieve slice, though.

Here, it’s about the clean separation:

  • Infrastructure translates external DTOs into entities.
  • ArticleResource encapsulates the technical resource lifecycle.
  • The store orchestrates named source signals and derives a read model.
  • ViewModel mapping stays a simple projection.
  • Presentation reacts to facade signals.

The mapping from entity to ViewModel lives at the state level.

Not in the infrastructure.

Not in the component.

import { Article } from '../entities/article.model';
import { ArticleVm } from './article.vm';
export const toArticleViewModel = (article: Article | null): ArticleVm | null => {
if (article === null) {
return null;
}
const { title, subtitle, heroImageUrl, author, paragraphs } = article;
return {
title,
subtitle,
heroImageUrl,
author: {
name: author.name,
avatarUrl: author.avatarUrl,
emailLabel: author.email,
},
paragraphs,
};
};

The mapper projects an optionally present article into an optionally present ViewModel.

It doesn’t interpret an HTTP status.

It doesn’t decide why there’s currently no article.

It only receives the already normalized source:

Article | null
ArticleVm | null

The domain entity itself stays strict.

Article contains no null, no empty required fields, and no technical loading state.

A second boundary becomes visible here:

Infrastructure:
DTO → Entity
HttpResourceRef → safe source signals
+state:
Entity | null → ViewModel | null

These aren’t the same mappers.

They answer different questions.

The infrastructure asks:

What did the API deliver, how do we translate it into our internal language, and how do we safely expose the technical lifecycle?

The state layer asks:

What does the UI need to render this article cleanly?

The store orchestrates the source signals.

import { computed, inject } from '@angular/core';
import { signalStore, withComputed, withMethods, withProps } from '@ngrx/signals';
import { ArticleResource } from '../infrastructure/article.resource';
import { toArticleViewModel } from './article-view-model.mapper';
export const ArticleStore = signalStore(
withProps(() => ({
_articleResource: inject(ArticleResource),
})),
withComputed(({ _articleResource }) => ({
isLoading: _articleResource.isLoading,
error: _articleResource.error,
vm: computed(() => toArticleViewModel(_articleResource.article())),
})),
withMethods(({ _articleResource }) => ({
reload: (): void => _articleResource.reload(),
})),
);

The read flow is now deliberately quiet.

You can see immediately:

article → ViewModel
isLoading → store API
error → store API
reload → infrastructure

There’s no more technical condition in between.

Not because the lifecycle vanished.

But because it already got normalized where it originates.

The store therefore knows neither:

  • hasValue()
  • the fact that value() can throw
  • ResourceStatus
  • the concrete HttpResourceRef
  • parse or request details

It consumes named signals and one explicit operation.

That’s the intended role of the store in this slice:

The store doesn’t interpret a technical resource lifecycle. It orchestrates sources, projections, and intents.

That doesn’t make it decision-free, though.

It still decides:

  • which signals the slice makes public
  • which ViewModel gets derived
  • which operations get offered as slice intents

But it contains no case-by-case decision about whether an Angular resource is currently safe to read.

This distinction improves the read flow.

Reading the store, you’re interested in the architecture of the read flow:

Source Signal → ViewModel Projection

Not the mechanics of the underlying framework API.

Publicly, the store only exposes:

  • isLoading
  • error
  • vm
  • reload()

The entity stays an internal input for the ViewModel mapping.

Presentation shouldn’t have to decide whether it uses the entity or the ViewModel. For this retrieve slice, the ViewModel is the public read API. The entity stays an internal intermediate step between infrastructure and +state.

The ACL in this example doesn’t return null.

If the external response can’t be translated into an Article, that’s not an empty article — it’s an error at the system boundary. In a production system, this spot would be secured by runtime validation. If that validation fails, the resource lands in the error state.

The store deliberately doesn’t build a complex page-state object here.

It provides isLoading, error, and vm separately. That’s more readable for this slice: the technical lifecycle is normalized in the infrastructure, while the ViewModel stays a simple projection of the optionally present article.

Presentation gets these signals later through the facade.

The foundational article Event-driven Projection describes state as a projection. In this slice, withComputed is the place where this projection becomes visible.


In strict DDD, you’d expect a use case here.

For example:

LoadArticleUseCase

The use case would load data, prepare it in domain terms, and return a result.

In this read slice, that would often be artificial, though.

Why?

Because the NgRx Signal Store, with withComputed, already takes over a large part of the composition:

  • consuming named source signals
  • providing loading and error state as the slice API
  • projecting the ViewModel
  • offering reload as a slice intent

An additional application use case here would probably just pass through what the store already models cleanly.

That’s why the application layer is deliberately thin in this example.

Not because the application layer isn’t important.

But because this concrete read flow doesn’t need a heavy use case.

import { Injectable, inject } from '@angular/core';
import { ArticleStore } from '../+state/article.store';
@Injectable()
export class ArticleFacade {
private readonly store = inject(ArticleStore);
readonly vm = this.store.vm;
readonly isLoading = this.store.isLoading;
readonly error = this.store.error;
readonly reload = (): void => this.store.reload();
}

This facade doesn’t do much.

That’s exactly the point.

It defines the public API of the page:

vm()
isLoading()
error()
reload()

The facade here only passes through the signals and operations that presentation genuinely needs.

It doesn’t pass through the entity.

For this retrieve slice, the ViewModel is the public read API. The entity stays an internal intermediate step between infrastructure and +state.

Presentation shouldn’t need to know that an NgRx Signal Store, an infrastructure adapter, and a private httpResource sit behind it.

It only knows the facade.


4. Presentation: render signals, don’t orchestrate

Section titled “4. Presentation: render signals, don’t orchestrate”

Presentation is boring now.

That’s good.

It injects the facade, provides providers for the slice, and renders the ViewModel.

import { Component, inject } from '@angular/core';
import { ArticleStore } from '../+state/article.store';
import { ArticleFacade } from '../application/article.facade';
import { ArticleResource } from '../infrastructure/article.resource';
@Component({
selector: 'app-article-page',
templateUrl: './article-page.component.html',
providers: [ArticleResource, ArticleStore, ArticleFacade],
})
export class ArticlePageComponent {
protected readonly facade = inject(ArticleFacade);
}

The provider scope is deliberately shown locally on the page here.

This page slice gets its own resource, its own store, and its own facade.

In a real application, I’d often attach these providers to the route instead. For the article, though, the local variant directly on the component is helpful because it keeps the slice visibly self-contained.

What matters isn’t whether the providers sit on the route or on the page.

What matters is:

ArticleResource
ArticleStore
ArticleFacade

together form the scope of this retrieve slice.

The component itself stays thin. It only injects the facade and renders its signals in the template.

@let vm = facade.vm();
<article class="article-page">
@if (facade.error()) {
<p role="alert">The article could not be loaded.</p>
<button type="button" (click)="facade.reload()">
Try again
</button>
}
@if (facade.isLoading()) {
<p class="article-page__loading">
Updating article …
</p>
}
@if (vm) {
<header class="article-page__header">
<img
class="article-page__image"
[src]="vm.heroImageUrl"
[alt]="vm.title"
/>
<h1>{{ vm.title }}</h1>
<p class="article-page__subtitle">
{{ vm.subtitle }}
</p>
<div class="article-page__author">
<img
class="article-page__author-avatar"
[src]="vm.author.avatarUrl"
[alt]="vm.author.name"
/>
<div>
<p>{{ vm.author.name }}</p>
<p>{{ vm.author.emailLabel }}</p>
</div>
</div>
</header>
<div class="article-page__content">
@for (paragraph of vm.paragraphs; track paragraph) {
<p>{{ paragraph }}</p>
}
</div>
}
</article>

The template binds the ViewModel with @let.

That makes facade.vm() visible once as a local template variable, and the markup part stays quieter.

error and isLoading are deliberately handled separately. They’re resource states, not properties of ArticleVm.

isLoading doesn’t clear away the content. If a ViewModel already exists and a reload is running, the template can show a small hint, spinner, or progress indicator while the article stays visible.

That prevents unnecessary flickering.

The presentation logic knows:

  • no DTO
  • no URL
  • no HttpResourceRef
  • no hasValue()
  • no parse
  • no toDomain
  • no toViewModel
  • no technical loading-state logic

It binds facade signals and renders.

That’s no coincidence.

That’s the boundary.


Once more, from bottom to top:

1. The API delivers ArticleDto
2. infrastructure runs parseArticleResponse()
3. infrastructure maps DTO → Entity
4. the private httpResource holds the technical resource lifecycle
5. ArticleResource normalizes it into article, isLoading, error, and reload
6. +state projects Article | null → ArticleVm | null
7. +state provides isLoading, error, vm, and reload as the slice API
8. application passes through the public page API
9. presentation renders facade.vm()

The decisive thing isn’t that every file is particularly complicated.

On the contrary.

The files are small.

But each file has a different question.

article.resource.ts
Which external source gets loaded, and which safe signal API does the infrastructure provide?
article.mapper.ts
How does external structure get translated into internal language?
article.store.ts
How do source signals, projections, and intents get orchestrated?
article-view-model.mapper.ts
What does state look like for the UI?
article.facade.ts
What is the page allowed to consume?
article-page.component.html
How does the ViewModel get displayed?

You could write the entire slice into the store.

HTTP access, DTO mapping, resource lifecycle, ViewModel mapping, error state, reload.

That would be fewer files.

But fewer files aren’t automatically less complexity.

They just hide the complexity in one place.

The store shouldn’t become the new god class in this design.

It has a clear job:

It orchestrates named sources, projections, and intents.

The infrastructure encapsulates its own technical implementation.

That way, reading the store shows the read flow — not the mechanics of httpResource.


Why not put everything in the application layer?

Section titled “Why not put everything in the application layer?”

You could also argue:

In DDD, the use case belongs in the application layer. So the application layer should build the read flow here.

That would be theoretically cleaner.

But not necessarily better in this concrete Angular slice.

A pure retrieve flow with httpResource, signals, and the NgRx Signal Store already provides a clear reactive composition. If the application layer then only contains a use case that reads a signal from the store and returns it as a signal again, that additional layer does not create a meaningful architectural boundary.

It creates theater.

That’s why the application layer is thin here.

For more complex flows, that can look different:

  • coordinating multiple stores
  • checking permissions
  • combining parameters from routing and user context
  • building a domain-specific read use case
  • merging multiple resources into one domain view

Then the application layer deserves more weight.

This article deliberately shows the small read slice.

Not the maximal architecture apparatus.


Retrieve is more than GET and display.

In this design, httpResource belongs to the infrastructure because it models external access and a technical asynchronous lifecycle.

The concrete HttpResourceRef stays private in the infrastructure.

parse is the transformation boundary here. Runtime validation can be added later, but it isn’t the core of this slice.

An existing domain model must be valid. An artificial empty model is no substitute for cleanly modeled absence in single entities.

The infrastructure provides safe, named signals and explicit operations.

The store doesn’t interpret a technical resource lifecycle.

It orchestrates source signals, ViewModel projections, and intents.

parse -> toDomain() represents a different boundary from toViewModel().

The application layer is allowed to stay deliberately thin for simple read flows.

Presentation renders signals instead of orchestrating loading, mapping, and infrastructure logic.