Pular para o conteúdo principal

Transformando dados no fetch

Todas as requisições de rede passam pelo método fetch(), então qualquer transformação necessária pode simplesmente ser feita sobrescrevendo-o com uma chamada a super.

dica

Observação: se você mantém o controle sobre o design da API, geralmente é preferível atualizar os dados enviados pela rede. Manter o cliente o mais enxuto (thin) possível ajuda tanto no desempenho quanto na complexidade.

Dito isso, em muitos casos você quer consumir APIs sobre as quais não tem controle - seja por serem APIs públicas ou por causa da estrutura organizacional interna.

De snake para camel​

É comum que APIs sejam projetadas com chaves em snake_case, mas muitos em typescript/javascript preferem camelCase. Este trecho nos permite fazer a transformação necessária.

CamelResource.ts
import { camelCase, snakeCase } from 'lodash';
import { RestEndpoint, RestGenerics } from '@data-client/rest';

function deeplyApplyKeyTransform(obj: any, transform: (key: string) => string) {
const ret: Record<string, any> = Array.isArray(obj) ? [] : {};
Object.keys(obj).forEach(key => {
if (obj[key] != null && typeof obj[key] === 'object') {
ret[transform(key)] = deeplyApplyKeyTransform(obj[key], transform);
} else {
ret[transform(key)] = obj[key];
}
});
return ret;
}

class CamelEndpoint<O Extends RestGenerics = any> extends RestEndpoint<O> {
getRequestInit(body) {
// we'll need to do the inverse operation when sending data back to the server
if (body) {
return super.getRequestInit(deeplyApplyKeyTransform(body, snakeCase));
}
return super.getRequestInit(body);
}
process(value) {
return deeplyApplyKeyTransform(value, camelCase);
}
}

Desserializando campos​

Em muitos casos, os dados enviados via JSON são serializados como strings, já que o JSON tem apenas alguns tipos primitivos. Exemplos comuns incluem ISO 8601 para datas ou até strings para decimais que exigem alta precisão (floats podem perder precisão). Manter os dados na forma serializada costuma ser aceitável, especialmente se forem usados apenas para exibição. No entanto, isso pode ser problemático quando dados derivados são computados, como somar tempo a uma data ou multiplicar dois números.

Nesse caso, basta usar o static schema com Temporal.Instant e BigNumber

Fixtures
GET /price/btc-usd
{"exchangePair":"btc-usd","price":"32982389239823983298329832.238923982389328932893298","updatedAt":"2026-01-01T12:00:00.000Z"}
▶api/Price
import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';
import BigNumber from 'bignumber.js';

export class ExchangePrice extends Entity {
  exchangePair = '';
  updatedAt = Temporal.Instant.fromEpochMilliseconds(0);
  price = new BigNumber(0);
  pk() {
    return this.exchangePair;
  }
  static key = 'ExchangePrice';

  static schema = {
    updatedAt: Temporal.Instant.from,
    price: BigNumber,
  };
}
export const getPrice = new RestEndpoint({
  path: '/price/:exchangePair',
  schema: ExchangePrice,
});
▶PricePage
import { useSuspense } from '@data-client/react';
import { getPrice } from './api/Price';

function PricePage() {
  const currentPrice = useSuspense(getPrice, {
    exchangePair: 'btc-usd',
  });
  return (
    <div>
      ${currentPrice.price.toFormat(2)} as of{' '}
      <time>
        {currentPrice.updatedAt.toLocaleString('en-US', { dateStyle: 'medium' })}
      </time>
    </div>
  );
}
render(<PricePage />);
Resultado
Store▶

Desserializando Date​

Caso queira usar o Date legado, você pode transformar o construtor em um schema de função.

export class ExchangePrice extends Entity {
exchangePair = '';
updatedAt = new Date(0);
price = new BigNumber(0);
pk() {
return this.exchangePair;
}
static key = 'ExchangePrice';

static schema = {
updatedAt: iso => new Date(iso),
price: BigNumber,
};
}

O caso do Id ausente​

Agora você quer se integrar a um ótimo site de streaming novo chamado mystreamsite.tv. Ele tem uma API simples para obter informações sobre as transmissões atuais. Você pode obter uma transmissão com o padrão de url https://mystreamsite.tv/[username]/. No entanto, por algum motivo, eles não retornam o username no corpo da resposta! Você quer poder se referir a ele, e ele é o único identificador que define a classe de forma única.

Podemos simplesmente extrair o username da própria url da requisição e adicioná-lo à resposta.

GET https://mystreamsite.tv/ntucker/
{
"title": "When I'm Grandmaster, I will play faster.",
"game": "Starcraft II",
"current_viewers": 1337,
"live": true
}
api/Stream.ts
const USERNAME_MATCHER = /.*\/([^\/]+)\/?/;

class Stream extends Entity {
username = '';
title = '';
game = '';
currentViewers = 0;
live = false;

pk() {
return this.username;
}
static key = 'Stream';
}

const getStream = new RestEndpoint({
urlPrefix: 'https://mystreamsite.tv',
path: '/:username',
schema: Stream,
process(value, { username }) {
value.username = username;
return value;
},
});

Preços de ticker​

Aqui está um exemplo do mundo real de uma API em que os dados do ticker não incluem sua chave primária product_id.

Usamos RestEndpoint.process() para adicionar o membro product_id a partir do seu argumento.

import { Entity, RestEndpoint } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';

export class Ticker extends Entity {
  product_id = '';
  trade_id = 0;
  price = 0;
  size = '0';
  time = Temporal.Instant.fromEpochMilliseconds(0);
  bid = '0';
  ask = '0';
  volume = '';

  pk(): string {
    return this.product_id;
  }
  static key = 'Ticker';

  static schema = {
    price: Number,
    time: Temporal.Instant.from,
  };
}

export const getTicker = new RestEndpoint({
  urlPrefix: 'https://api.exchange.coinbase.com',
  path: '/products/:productId/ticker',
  schema: Ticker,
  process(value, { productId }) {
    value.product_id = productId;
    return value;
  },
  pollFrequency: 2000,
});
Resultado
Store▶

Usando headers HTTP​

Os Headers HTTP são acessíveis na Response do fetch. RestEndpoint.fetchResponse() pode ser usado para construir um RestEndpoint.

Às vezes isso é usado para paginação baseada em cursor.

import { RestEndpoint, RestGenerics } from '@data-client/rest';

class GithubEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async parseResponse(response: Response) {
const results = await super.parseResponse(response);
if (
(response.headers && response.headers.has('link')) ||
Array.isArray(results)
) {
return {
link: response.headers.get('link'),
results,
};
}
return results;
}
}

Download de arquivos​

Para endpoints que retornam dados binários (arquivos, imagens, PDFs), defina content: 'blob'. O tipo de retorno é Blob e schema tem undefined como padrão (dados binários não são normalizáveis). Use dataExpiryLength: 0 para evitar manter blobs grandes em cache na memória.

downloadFile.ts
import { RestEndpoint } from '@data-client/rest';

export const downloadFile = new RestEndpoint({
path: '/files/:id/download',
content: 'blob',
dataExpiryLength: 0,
});
DownloadButton.tsx
import { useController } from '@data-client/react';
import { downloadFile } from './downloadFile';

function DownloadButton({ id }: { id: string }) {
const ctrl = useController();

const handleDownload = async () => {
const blob: Blob = await ctrl.fetch(downloadFile, { id });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'download';
a.click();
URL.revokeObjectURL(url);
};

return <button onClick={handleDownload}>Download</button>;
}

Para extrair o nome do arquivo do header Content-Disposition, sobrescreva parseResponse:

downloadFile.ts
import { RestEndpoint } from '@data-client/rest';

export const downloadFile = new RestEndpoint({
path: '/files/:id/download',
content: 'blob',
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});

Para respostas ArrayBuffer (úteis para processar dados binários em memória), use content: 'arrayBuffer' da mesma forma.

Renomeando campos​

Às vezes uma API pode mudar o nome de uma chave ou escolher um de que você não gosta. Claro que você tem padrões de nomenclatura muito melhores, então, em vez de alterar a definição da sua classe Resource e todo o seu código, você quer apenas remapear essa chave.

ArticleResource.ts
class RenamedEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getRequestInit(body) {
if (body && 'carrotsUsed' in body) {
const newBody = {
...body,
carrotsUSedIsThisNameTooLong: carrotsUsed,
};
delete newBody.carrotsUsed;
return super.getRequestInit(newBody);
}
return super.getRequestInit(body);
}
process(value) {
if ('carrotsUsedIsThisNameTooLong' in value) {
// ok to mutate jsonResponse since we control it
value.carrotsUsed = value.carrotsUsedIsThisNameTooLong;
delete value.carrotsUsedIsThisNameTooLong;
}
return value;
}
}