Utilizando o Resource API do Angular 19 em um pequeno buscador
A API Resource, lançada no Angular 19, integra-se aos Signals, permitindo que componentes reajam automaticamente a mudanças nos dados. Isso elimina a necessidade de gerenciar assinaturas de observables ou usar outras soluções complexas. Veja neste texto um exemplo prático aplicado a um buscador.

Ainda em modo experimental, o Resource lançado no Angular 19, tem o objetivo de efetuar operações de leitura, reagindo a mudanças de estado de um ou mais signals. Antes desse lançamento, a abordagem mais comum era utilizar o effect
para monitorar alterações em signals e, em seguida, chamar uma função para realizar uma consulta, como uma requisição a uma API. O Resource simplifica bastante esse processo e será o foco deste texto.
Note thatresource
is intended for read operations, not operations which perform mutations.resource
will cancel in-progress loads via theAbortSignal
when destroyed or when a new request object becomes available, which could prematurely abort mutations.
https://angular.dev/api/core/resource?tab=description
Para testar esse novo recurso, criei um pequeno e simples buscador. A ideia é que conforme o usuário digite, vamos alterando o valor de um signal e essa mudança dispare uma nova requisição à uma API. Consequentemente, o resultado da consulta é exibido na tela.
Construção do cenário de estudo
Para criar o cenário de estudo, utilizei dois projetos:
- frontend: um projeto Angular (versão 19)
- backend: uma pequena API com Node.js, sem frameworks.
Criando o projeto Angular
Para criar o projeto Angular, utilizei o seguinte comando e opções:
ng new --skip-git --skip-tests teste_resource
✔ Which stylesheet format would you like to use? Sass (SCSS)
[https://sass-lang.com/documentation/syntax#scss]
✔ Do you want to enable Server-Side Rendering (SSR) and
Static Site Generation (SSG/Prerendering)? No
Após criar o projeto, instalei o pacote Angular Material para ter acesso a uma ampla variedade de componentes visuais. Este pacote não é obrigatório para o uso do resource
!
# Acessando o diretório do projeto criado
cd teste_resource
# Adicionando o Angular Material
ng add @angular/material
Criando API para testes
Após criar o projeto Angular com os comandos acima, criei um diretório chamado /server
e adicionei dois arquivos que irão compor a pequena API:
/server
pode ficar em qualquer pasta de sua preferência. Não precisa necessariamente ficar no mesmo nível de diretório do projeto Angular.import { createServer } from 'node:http';
import url from 'url';
import carros from './dados.json' with { type: 'json' };
function delay(atrasoEmMS = 1000) {
return new Promise((resolve) => {
setTimeout(() => resolve(), atrasoEmMS);
});
}
const server = createServer(async (req, res) => {
// Esse atraso (delay) é proposital para facilitar a
// análise no frontend.
await delay();
const parsedUrl = url.parse(req.url, true);
const palavraPesquisada = parsedUrl.query['q']
.trim()
.toLocaleLowerCase();
const cabecalhosResposta = {
'Content-Type': 'application/json; charset=utf-8',
};
// Se o valor da query string `q` for `erro`,
// devolvemos o http status 500
if (palavraPesquisada === 'erro') {
res.writeHead(500, cabecalhosResposta);
res.end(JSON.stringify({ mensagem: 'Erro na API' }));
return;
}
const carrosFiltrados = carros.filter((a) =>
a.nome.toLocaleLowerCase().includes(palavraPesquisada),
);
res.writeHead(200, cabecalhosResposta);
res.end(JSON.stringify(carrosFiltrados));
});
server.listen(3000, () => console.log('Servidor online'));
index.mjs
Abaixo o arquivo JSON que é utilizado no código acima:
[
{
"id": 1,
"nome": "Toyota Corolla",
"descricao": "Sedan compacto confiável e eficiente."
},
{
"...": "...outros registros..."
},
{
"id": 49,
"nome": "Volvo XC90",
"descricao": "SUV de luxo com foco em segurança."
},
{
"id": 50,
"nome": "Lexus RX",
"descricao": "SUV premium com design refinado."
}
]
dados.json
Essa API de testes está preparada para receber requisições no seguinte formato:
# Pesquisa os carros que começam com "ford"
curl -i "http://localhost:3000?q=ford"
Resultado:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
[{"id":3,"nome":"Ford Mustang","descricao":"Clássico muscle car americano com potência impressionante."},{"id":16,"nome":"Ford Fusion","descricao":"Modelo híbrido com conforto e eficiência."},{"id":21,"nome":"Ford Explorer","descricao":"SUV médio com bastante espaço."},{"id":24,"nome":"Ford F-150","descricao":"Picape clássica com recursos modernos."},{"id":43,"nome":"Ford Edge","descricao":"Crossover com desempenho confiável."}]%
abortSignal
, que será explicado posteriormente.Agora que a pequena API de testes foi criada, seguimos com foco no projeto Angular.
Preparando o projeto Angular
Sem entrar nos detalhes, a seguir fiz algumas inclusões e alterações no projeto Angular. A ideia foi criar os arquivos necessários, ajustar o estilo visual e efetuar as devidas referências, veja a seguir:
Criando os recursos com Angular CLI:
# Cria o componente <app-buscador>
ng g c buscador
# Cria a interface Carro
ng g i carro
Conteúdo dos arquivos modificados e/ou recém criados:
import { Component } from '@angular/core';
import {
BuscadorComponent
} from './buscador/buscador.component';
@Component({
selector: 'app-root',
imports: [BuscadorComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {}
app.component
<div class="container">
<div class="row">
<div class="col">
<app-buscador></app-buscador>
</div>
</div>
</div>
app.component.html
html, body { height: 100%; }
body {
display: flex;
margin: 0;
width: 100vw;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.container {
width: 100vw;
margin: 0 auto;
}
.row {
display: flex;
flex-wrap: wrap;
}
.col {
flex: 1 1 auto;
padding: 15px;
}
styles.scss
export interface Carro {
id: number;
nome: string;
descricao: string;
}
carro.ts
Utilizando o resource
para efetuar uma consulta
O mecanismo de busca implementado a seguir funciona da seguinte forma: o usuário digita a palavra desejada em um campo de texto exibido na tela. Assim que o texto inserido atingir pelo menos 3 caracteres, o valor digitado será atribuído a uma variável do tipo WritableSignal
. Essa variável, ao ser modificada, dispara a execução da loader
, associada ao resource
.
Na implementação abaixo deixei vários comentários para facilitar o entendimento. Também incluí um tratamento de erro com o SnackBar do Angular Material.
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
Component,
computed,
effect,
ElementRef,
inject,
resource,
ResourceRef,
signal,
viewChild,
} from '@angular/core';
import {
MatFormFieldModule
} from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import {
MatProgressBarModule
} from '@angular/material/progress-bar';
import {
MatSnackBar,
MatSnackBarModule,
} from '@angular/material/snack-bar';
import {
debounceTime,
distinctUntilChanged,
fromEvent,
map,
} from 'rxjs';
import { Carro } from '../carro';
@Component({
selector: 'app-buscador',
imports: [
CommonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatListModule,
MatProgressBarModule,
MatSnackBarModule,
],
templateUrl: './buscador.component.html',
styleUrl: './buscador.component.scss',
})
export class BuscadorComponent implements AfterViewInit {
palavraPesquisada = signal<string | null>(null);
inputBuscador =
viewChild.required<ElementRef<HTMLInputElement>>(
'buscador',
);
private snackBar = inject(MatSnackBar);
carrosResource: ResourceRef<Carro[]> = resource({
request: () => ({
palavraProcurada: this.palavraPesquisada(),
}),
loader: async ({ request, abortSignal }) => {
const { palavraProcurada } = request;
const efetuarConsulta =
palavraProcurada !== null &&
palavraProcurada.length >= 3;
if (!efetuarConsulta) {
return Promise.resolve([]);
}
const url = new URL('', 'http://localhost:3000');
url.searchParams.append('q', palavraProcurada);
const resposta = await fetch(url, {
signal: abortSignal,
});
// A Promise retornada do fetch() não rejeitará o
// status do erro HTTP, mesmo que a resposta seja
// um HTTP 404 ou 500. Em vez disso, ela irá resolver
// normalmente (com o status ok definido como falso),
// e só irá rejeitar se houver falha na rede ou se
// algo impedir a requisição de ser completada.
//
// https://developer.mozilla.org/pt-
// BR/docs/Web/API/Fetch_API/Using_Fetch
if (!resposta.ok) {
const { mensagem } = await resposta.json();
throw mensagem;
}
return resposta.json();
},
});
estaCarregando = computed(() =>
this.carrosResource.isLoading(),
);
resultadoBusca = computed(
() => this.carrosResource.value() ?? [],
);
constructor() {
effect(() => {
const erro = this.carrosResource.error();
if (erro) {
const mensagem =
typeof erro === 'string' ? erro : 'Erro genérico';
this.snackBar.open(mensagem, '', { duration: 3e3 });
}
});
}
ngAfterViewInit(): void {
this.escutarEventoInputBusca();
}
escutarEventoInputBusca() {
fromEvent(this.inputBuscador().nativeElement, 'input')
.pipe(
// Aguarda 250ms após a última alteração de valor
// do input, antes de emitir o evento.
debounceTime(250),
// Abaixo propagamos somente o `.value` do input
map((event) =>
(event.target as HTMLInputElement).value.trim(),
),
// Emite o evento somente quando houver uma
// alteração de valor.
// A ideia é que o "trim()" em conjunto com
// o `distinctUntilChanged`, evite disparar
// consultas quando o usuário adicionar um
// espaço no final da palavra buscada.
distinctUntilChanged(),
)
.subscribe((valorDoInputBuscador) => {
// Por fim, atualizamos o `signal` palavraPesquisada.
// Essa atualização fará com que o `carrosResource`
// execute a função `loader`.
this.palavraPesquisada.set(valorDoInputBuscador);
});
}
}
buscador.component.ts
<div class="row">
<div class="col">
<mat-form-field style="width: 100%">
<mat-label>Buscador:</mat-label>
<input
#buscador
matInput
type="text"
type="search"
autofocus />
</mat-form-field>
@if (estaCarregando()) {
<mat-progress-bar
mode="indeterminate"></mat-progress-bar>
} @else {
<p>
Quantidade de registros encontrados:
<strong>{{ resultadoBusca().length }}</strong>
</p>
<mat-list>
@for (carro of resultadoBusca(); track carro.id) {
<mat-list-item>
<mat-icon
matListItemIcon
fontIcon="directions_car"></mat-icon>
<div matListItemTitle>{{ carro.nome }}</div>
<div matListItemLine>{{ carro.descricao }}</div>
</mat-list-item>
} @empty {
<p>Nenhum registro encontrado!</p>
}
</mat-list>
}
</div>
</div>
buscador.component.html
Resultado:

Uso do abortSignal
Em um post mais antigo, comentei sobre o uso do AbortController para aplicar timeouts ou cancelar requisições.

No caso do resource
, sempre que o valor retornado pela request
(declarado dentro do resource
) é alterado, ele executa novamente o loader
. Nesse cenário, o abortSignal
é utilizado para que o resource
cancele a execução anterior, caso ela ainda esteja em andamento, antes de iniciar uma nova. Abaixo, observe o comportamento das requisições sem o uso do abortSignal e com ele:

resource
e fetch
sem o uso do abortSignal
Observe acima que, à medida que o usuário digita pausadamente, várias requisições são disparadas sem que nenhuma delas seja cancelada. Por outro lado, ao utilizar o abortSignal, à medida que o usuário digita pausadamente, a requisição anterior é cancelada caso ainda esteja em andamento:

abortSignal
com o resource
Considerações
Ainda em modo experimental, dependendo da resposta da comunidade e do time de desenvolvimento do Angular, pode ser que essa funcionalidade seja alterada. Então use com cuidado enquanto estiver em modo experimental.
A capacidade de reatividade do signal
é incrível. Até então, tínhamos que "escutar" por alterações de valores e então iniciar o fluxo para obter dados externos, como de uma API. O resource
claramente simplificou esse processo, além da capacidade de cancelar requisições pendentes através do abortSignal
, que é um diferencial importante, evitando problemas comuns em aplicações que lidam com múltiplas requisições.