Pular para o conteúdo principal

Migrando do Axios

O @data-client/rest substitui o axios por uma abordagem declarativa e com tipagem segura para APIs REST.

Migração assistida por IA​

Instale a skill de configuração do REST para automatizar a migração com seu assistente de código com IA. Ela detecta automaticamente o axios no seu projeto e executa o codemod para as transformações determinísticas; depois, guia você pelos passos manuais que exigem julgamento (interceptors, tratamento de erros, definições de schema etc.).

npx skills add reactive/data-client \
--skill data-client-schema \
--skill data-client-rest-setup \
--skill data-client-rest

Em seguida, execute a skill /data-client-rest-setup para iniciar a migração. Ela detectará o axios e aplicará automaticamente o subprocedimento de migração adequado.

Por que migrar?​

Paths com tipagem segura​

Com o axios, os paths da API são strings opacas — erros de digitação e parâmetros ausentes só são detectados em tempo de execução:

// axios: no type checking — typo silently produces wrong URL
axios.get(`/users/${usrId}`);

Com o RestEndpoint, os parâmetros do path são inferidos a partir do template de path e verificados em tempo de compilação:

const getUser = new RestEndpoint({ path: '/users/:id', schema: User });
// TypeScript enforces { id: string } — typos are compile errors
getUser({ id: '1' });

Isso também significa que o autocompletar da IDE funciona para todos os parâmetros do path.

Benefícios adicionais​

  • Cache normalizado — entities compartilhadas são deduplicadas e atualizadas automaticamente em todos os lugares
  • Dependências de dados declarativas — os componentes declaram quais dados precisam por meio de useSuspense(), e não como buscá-los
  • Atualizações otimistas — feedback instantâneo na UI antes de o servidor responder
  • Zero código boilerplate — resource() gera uma API CRUD completa a partir de um path e de um schema

Referência rápida​

Axios@data-client/rest
baseURLurlPrefix
configuração headersgetHeaders()
interceptors.requestgetRequestInit() / getHeaders()
interceptors.responseparseResponse() / process()
timeoutAbortSignal.timeout() via signal
params / paramsSerializersearchParams / searchToString()
cancelToken / signalsignal (AbortController)
responseType: 'blob' / 'arraybuffer'content: 'blob' / 'arrayBuffer' — veja download de arquivo
auth: { username, password }getHeaders() com btoa()
xsrfCookieName / xsrfHeaderNamegetHeaders() — veja Integração com Django
transformRequestgetRequestInit()
transformResponseprocess()
validateStatusfetchResponse() personalizado
onUploadProgressfetchResponse() personalizado usando XMLHttpRequest
isAxiosError / error.responseNetworkError com .status e .response

Exemplos de migração​

GET básico​

api.ts
import axios from 'axios';

export const getUser = (id: string) =>
axios.get(`https://api.example.com/users/${id}`);
usage.ts
const { data } = await getUser('1');

Instância com URL base e headers​

api.ts
import axios from 'axios';

const api = axios.create({
baseURL: 'https://api.example.com',
headers: { 'X-API-Key': 'my-key' },
});

export const getPost = (id: string) => api.get(`/posts/${id}`);
export const createPost = (data: any) => api.post('/posts', data);

Mutação com POST​

api.ts
import axios from 'axios';

const api = axios.create({ baseURL: 'https://api.example.com' });

export const createPost = (data: { title: string; body: string }) =>
api.post('/posts', data);

Interceptors → métodos de ciclo de vida​

Os interceptors do axios correspondem aos métodos de ciclo de vida do RestEndpoint:

api.ts
import axios from 'axios';

const api = axios.create({ baseURL: 'https://api.example.com' });

// Request interceptor — add auth token
api.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});

// Response interceptor — unwrap .data
api.interceptors.response.use(
response => response.data,
error => Promise.reject(error),
);
dica

O RestEndpoint já retorna o JSON interpretado por padrão — não é necessário nenhum interceptor para extrair response.data.

Interceptors de resposta que transformam o corpo, como a conversão de chaves em snake_case, pertencem a process(). Veja snakes to camels para um exemplo completo.

Tratamento de erros​

import axios from 'axios';

try {
const { data } = await axios.get('/users/1');
} catch (err) {
if (axios.isAxiosError(err)) {
console.log(err.response?.status);
console.log(err.response?.data);
}
}

Mensagens de erro do servidor​

Bases de código com axios costumam expor error.response.data.error ou .message ao usuário. Em vez disso, leia isso do corpo da Response uma única vez, no fetchResponse() da classe base, para que os pontos de chamada obtenham a mensagem em error.message sem precisar interpretar o corpo:

ApiEndpoint.ts
import {
NetworkError,
RestEndpoint,
RestGenerics,
} from '@data-client/rest';

export default class ApiEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async fetchResponse(input: RequestInfo, init: RequestInit) {
try {
return await super.fetchResponse(input, init);
} catch (error) {
if (error instanceof NetworkError) {
const body = await error.response
.clone()
.json()
.catch(() => null);
// keep the NetworkError so `status` and `errorPolicy()` still work
error.message = body?.error ?? body?.message ?? error.message;
}
throw error;
}
}
}

Cancelamento​

import axios from 'axios';

const controller = new AbortController();
axios.get('/users', { signal: controller.signal });
controller.abort();

Ou com o CancelToken, já obsoleto:

const source = axios.CancelToken.source();
axios.get('/users', { cancelToken: source.token });
source.cancel();

Veja o guia de abort para mais padrões.

Timeout​

Before (axios)
axios.get('/users', { timeout: 5000 });
After (data-client)
const getUsers = new RestEndpoint({
path: '/users',
signal: AbortSignal.timeout(5000),
});

Respostas binárias​

Before (axios)
axios.get('/files/1', { responseType: 'blob' });

Defina content como 'blob', 'arrayBuffer' ou 'text'. Veja download de arquivo para o endpoint completo e para disparar um download no navegador.

Serialização de query​

Before (axios)
axios.get('/users', {
params: { ids: [1, 2, 3] },
paramsSerializer: params =>
qs.stringify(params, { arrayFormat: 'repeat' }),
});

Sobrescreva searchToString() para serializar com qs; veja usando a biblioteca qs.

Autenticação básica​

Before (axios)
axios.get('/api', { auth: { username: 'user', password: 'pass' } });
After (data-client)
export default class BasicAuthEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getHeaders(headers: HeadersInit) {
return {
...headers,
Authorization: `Basic ${btoa('user:pass')}`,
};
}
}

Aceitando status de erro​

fetchResponse() lança NetworkError para qualquer status diferente de ok. Sobrescreva-o para mudar o que conta como erro:

Before (axios)
axios.get('/api', { validateStatus: status => status < 500 });
After (data-client)
import {
NetworkError,
RestEndpoint,
RestGenerics,
} from '@data-client/rest';

export default class LenientEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async fetchResponse(input: RequestInfo, init: RequestInit) {
const response = await fetch(input, init);
if (response.status >= 500) throw new NetworkError(response);
return response;
}
}

Headers CSRF​

Before (axios)
axios.create({
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken',
});

Leia o cookie em getHeaders() para requisições que não sejam GET. Veja Integração com Django para a classe de endpoint completa.

Progresso de upload​

O fetch não consegue informar o progresso de upload, então use XMLHttpRequest dentro de fetchResponse(). O campo onProgress é passado como uma opção do endpoint, como qualquer outro membro.

Before (axios)
axios.post('/upload', formData, {
onUploadProgress: e => console.log(e.loaded / e.total),
});
After (data-client)
import {
NetworkError,
RestEndpoint,
RestGenerics,
} from '@data-client/rest';

export default class UploadEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
declare onProgress?: (progress: number) => void;

fetchResponse(input: RequestInfo, init: RequestInit) {
return new Promise<Response>((resolve, reject) => {
const xhr = new XMLHttpRequest();
const abort = () => xhr.abort();
const abortError = () =>
new DOMException('The operation was aborted.', 'AbortError');
if (init.signal?.aborted) return reject(abortError());
init.signal?.addEventListener('abort', abort, { once: true });

xhr.open(
init.method ?? 'POST',
typeof input === 'string' ? input : input.url,
);
new Headers(init.headers).forEach((value, key) =>
xhr.setRequestHeader(key, value),
);
xhr.onloadend = () =>
init.signal?.removeEventListener('abort', abort);
xhr.upload.onprogress = e => {
if (e.lengthComputable) this.onProgress?.(e.loaded / e.total);
};
xhr.onload = () => {
const headers = new Headers();
for (const line of xhr
.getAllResponseHeaders()
.trim()
.split(/\r?\n/)) {
const [key, ...rest] = line.split(': ');
if (key) headers.append(key, rest.join(': '));
}
// 204, 205 and 304 responses can't have a body
const body = [204, 205, 304].includes(xhr.status)
? null
: xhr.response;
const response = new Response(body, {
status: xhr.status,
statusText: xhr.statusText,
headers,
});
if (response.ok) resolve(response);
else reject(new NetworkError(response));
};
xhr.onerror = () => reject(new TypeError('Network request failed'));
xhr.onabort = () => reject(abortError());
xhr.send(init.body as XMLHttpRequestBodyInit | null);
});
}
}

const uploadFile = new UploadEndpoint({
path: '/upload',
method: 'POST',
body: {} as FormData,
onProgress: (progress: number) => console.log(progress),
});

Codemod​

Um codemod independente do jscodeshift cuida das partes mecânicas da migração. Execute-o você mesmo em fluxos de trabalho sem IA; a skill de IA acima o executa automaticamente como primeiro passo.

npx jscodeshift -t https://dataclient.io/codemods/axios-to-rest.js --extensions=ts,tsx,js,jsx src/

O codemod, automaticamente:

  • Substitui import axios from 'axios' por import { RestEndpoint } from '@data-client/rest'
  • Converte axios.create({ baseURL, headers }) em uma subclasse base de RestEndpoint com urlPrefix e getHeaders()
  • Transforma axios.get(), .post(), .put(), .patch(), .delete() em new RestEndpoint({ path, method })
  • Transforma chamadas em uma instância criada (api.post(), em que api = axios.create(...)) em new CreatedClassName({ path, method })

O codemod tem pouco a fazer quando o projeto encapsula o axios em uma classe ou função própria e nunca chama axios.get()/.post() diretamente, ou só chama axios(config) sem um nome de método. Nesses casos, pule-o e comece pelos passos manuais.

O codemod não trata:

Encontrando usos restantes do axios​

Padrões de busca para localizar o que ainda precisa ser migrado:

PadrãoEncontra
import.*from ['"]axios['"]instruções de import
axios\.createcriação de instância
axios\.(get|post|put|patch|delete)chamadas diretas
\.interceptors\.(request|response)\.useinterceptors
isAxiosErrortratamento de erros
cancelToken|CancelTokencancelamento (obsoleto)
onUploadProgress|onDownloadProgresscallbacks de progresso

Depois do codemod​

O codemod produz endpoints sem schemas. Definir schemas de Entity e ligá-los aos endpoints habilita a normalização e o cache — o principal valor do Reactive Data Client.

Chaves primárias fora do padrão​

Muitas APIs (MongoDB, por exemplo) usam _id em vez de id. Sobrescreva pk():

import { Entity } from '@data-client/rest';

export class User extends Entity {
_id = '';
name = '';
email = '';
static key = 'User';

pk() {
return this._id;
}
}

Agrupe endpoints CRUD com resource()​

Quando um módulo axios tem funções separadas getUsers, getUser, createUser, updateUser e deleteUser para um mesmo path, substitua-as por um único resource():

import { resource } from '@data-client/rest';
import ApiEndpoint from './ApiEndpoint';
import { User } from './User';

export const UserResource = resource({
path: '/users/:id',
schema: User,
Endpoint: ApiEndpoint,
});
// UserResource.getList, .get, .getList.push, .update, .partialUpdate, .delete

Paths aninhados como /projects/:projectId/tasks/:taskId ganham o seu próprio resource. Reserve new ApiEndpoint() avulso para operações que não são CRUD (busca, ações personalizadas, autenticação).

Convivendo com Zod ou Yup​

Se a base de código já valida respostas com Zod ou Yup, escolha uma abordagem por tipo:

  • Zod em process() (recomendado): mantenha a validação em tempo de execução fazendo o parse em process() e deixe a Entity cuidar da normalização:

    const getUser = new ApiEndpoint({
    path: '/users/:id',
    schema: User,
    process(value: any) {
    return userSchema.parse(value);
    },
    });
  • Entity substitui o Zod: mova o formato dos campos para a classe Entity e remova o schema do Zod. Os campos da Entity fornecem tipos, não verificações em tempo de execução, então adicione static validate() para os campos que o servidor possa enviar malformados.

  • Somente Zod, sem Entity: deixe schema indefinido e faça o parse manualmente. Faça isso apenas em endpoints que não se beneficiam da normalização (tokens de autenticação, respostas pontuais).

aviso

Não defina classes Entity e depois deixe schema indefinido em todos os endpoints — sem schema, nada é normalizado e a migração ganha pouco em relação ao axios.

Tipagem do body​

Tipe o body de endpoints POST/PUT/PATCH avulsos com body: {} as BodyType. Não use undefined as unknown as BodyType: o RestEndpoint trata body: undefined como ausência de argumento de body.

const createUser = new ApiEndpoint({
path: '/users',
method: 'POST',
body: {} as { name: string; email: string },
schema: User,
});

resource() tipa seus endpoints CRUD automaticamente.

Converta os pontos de chamada em hooks​

import { useEffect, useState } from 'react';
import api from './lib/api';
import { Spinner } from './Spinner';
import type { User } from './User';

function UserProfile({ id }: { id: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
api.get(`/users/${id}`).then(({ data }) => setUser(data));
}, [id]);
if (!user) return <Spinner />;
return <h1>{user.name}</h1>;
}

Os estados de carregamento e de erro passam para o AsyncBoundary. Veja useSuspense() para mais detalhes.

Autenticação baseada em contexto​

Quando os tokens vêm do contexto do React (Okta, Auth0) em vez de um armazenamento, use hookifyResource() para injetar headers por meio de um hook. Veja o guia de autenticação para este e outros padrões.

Migração gradual​

Se o app usa TanStack Query ou SWR e não consegue converter tudo de uma vez, mantenha esses hooks temporariamente, mas faça o fetch por meio de controller.fetch(). Chamar um endpoint diretamente apenas executa o seu fetch; passar pelo Controller também normaliza a resposta no cache compartilhado, de modo que os dados ficam consistentes desde o primeiro dia:

import { useController } from '@data-client/react';
import { useQuery } from '@tanstack/react-query';
import ApiEndpoint from './ApiEndpoint';
import { Project } from './Project';

export const getProject = new ApiEndpoint({
path: '/projects/:id',
schema: Project,
});

export function useProject(id: string) {
const ctrl = useController();
return useQuery({
queryKey: ['project', id],
queryFn: () => ctrl.fetch(getProject, { id }),
});
}

Mais tarde, substitua useProject(id) por useSuspense(getProject, { id }).

Abstrações de endpoint existentes​

Bases de código que já têm uma classe de endpoint personalizada encapsulando o axios (digamos, uma com path, method e um helper toDynamicUrl()) podem estender RestEndpoint em vez de substituí-lo, mantendo métodos compatíveis com versões anteriores e ganhando url(), getRequestInit(), fetchResponse() e parseResponse():

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

export class LegacyEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = API_ROOT;
declare queryKey?: string;

/** @deprecated use url() */
toDynamicUrl = this.url;
}

const getUser = new LegacyEndpoint({
path: '/users/:id',
queryKey: 'user',
schema: User,
});

Passe membros extras como queryKey como opções, em vez de usar um construtor personalizado, para que extend() (usado por resource(), hookifyResource() e useCancelling()) continue funcionando.