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.).
- Skills
- OpenSkills
- Claude Code
npx skills add reactive/data-client \
--skill data-client-schema \
--skill data-client-rest-setup \
--skill data-client-rest
npx openskills install reactive/data-client/.agents/skills/data-client-schema
npx openskills install reactive/data-client/.agents/skills/data-client-rest-setup
npx openskills install reactive/data-client/.agents/skills/data-client-rest
claude plugin marketplace add reactive/data-client
claude plugin install core@data-client
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 umpathe de umschema
Referência rápida
| Axios | @data-client/rest |
|---|---|
baseURL | urlPrefix |
configuração headers | getHeaders() |
interceptors.request | getRequestInit() / getHeaders() |
interceptors.response | parseResponse() / process() |
timeout | AbortSignal.timeout() via signal |
params / paramsSerializer | searchParams / searchToString() |
cancelToken / signal | signal (AbortController) |
responseType: 'blob' / 'arraybuffer' | content: 'blob' / 'arrayBuffer' — veja download de arquivo |
auth: { username, password } | getHeaders() com btoa() |
xsrfCookieName / xsrfHeaderName | getHeaders() — veja Integração com Django |
transformRequest | getRequestInit() |
transformResponse | process() |
validateStatus | fetchResponse() personalizado |
onUploadProgress | fetchResponse() personalizado usando XMLHttpRequest |
isAxiosError / error.response | NetworkError com .status e .response |
Exemplos de migração
GET básico
- Before (axios)
- After (data-client)
import axios from 'axios';
export const getUser = (id: string) =>
axios.get(`https://api.example.com/users/${id}`);
const { data } = await getUser('1');
import { RestEndpoint } from '@data-client/rest'; import User from './User'; export const getUser = new RestEndpoint({ urlPrefix: 'https://api.example.com', path: '/users/:id', schema: User, });
import { getUser } from './api'; getUser({ id: '1' });
GET https://api.example.com/users/1
content-type: application/json
{
"id": "1",
"username": "alice",
}
Instância com URL base e headers
- Before (axios)
- After (data-client)
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);
import { RestEndpoint, RestGenerics } from '@data-client/rest'; export default class ApiEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { urlPrefix = 'https://api.example.com'; getHeaders(headers: HeadersInit) { return { ...headers, 'X-API-Key': 'my-key', }; } }
import { PostResource } from './PostResource'; PostResource.get({ id: '1' });
GET https://api.example.com/posts/1
content-type: application/json
x-api-key: my-key
{
"id": "1",
"title": "Hello World",
"body": "First post"
}
Mutação com POST
- Before (axios)
- After (data-client)
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);
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ urlPrefix: 'https://api.example.com', path: '/posts/:id', schema: Post, });
import { PostResource } from './PostResource'; PostResource.getList.push({ title: 'New Post', body: 'Content', });
POST https://api.example.com/posts
content-type: application/json
Body: {"title":"New Post","body":"Content"}
{
"id": "2",
"title": "New Post",
"body": "Content"
}
Interceptors → métodos de ciclo de vida
Os interceptors do axios correspondem aos métodos de ciclo de vida do RestEndpoint:
- Before (axios)
- After (data-client)
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),
);
import { RestEndpoint, RestGenerics } from '@data-client/rest';
export default class ApiEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = 'https://api.example.com';
// Equivalent to request interceptor
getHeaders(headers: HeadersInit) {
return {
...headers,
Authorization: `Bearer ${getToken()}`,
};
}
// Equivalent to response interceptor (unwrap/transform)
process(value: any, ...args: any) {
return value;
}
}
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
- Before (axios)
- After (data-client)
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);
}
}
import { NetworkError } from '@data-client/rest';
try {
const user = await getUser({ id: '1' });
} catch (err) {
if (err instanceof NetworkError) {
console.log(err.status);
console.log(err.response);
}
}
NetworkError fornece .status e .response (o objeto Response bruto). Para novas tentativas suaves em erros de servidor, veja errorPolicy.
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:
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
- Before (axios)
- After (data-client)
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();
Ambos correspondem a um signal de AbortController. O hook useCancelling() cancela automaticamente as requisições em andamento quando os parâmetros mudam:
import { useSuspense } from '@data-client/react';
import { useCancelling } from '@data-client/react';
import { searchEndpoint } from './api/search';
import ResultsList from './ResultsList';
function SearchResults({ query }: { query: string }) {
const results = useSuspense(useCancelling(searchEndpoint), { q: query });
return <ResultsList results={results} />;
}
Para cancelamento manual, passe signal diretamente:
const controller = new AbortController();
const getUser = new RestEndpoint({
path: '/users/:id',
signal: controller.signal,
});
controller.abort();
Veja o guia de abort para mais padrões.
Timeout
axios.get('/users', { timeout: 5000 });
const getUsers = new RestEndpoint({
path: '/users',
signal: AbortSignal.timeout(5000),
});
Respostas binárias
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
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
axios.get('/api', { auth: { username: 'user', password: 'pass' } });
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:
axios.get('/api', { validateStatus: status => status < 500 });
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
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.
axios.post('/upload', formData, {
onUploadProgress: e => console.log(e.loaded / e.total),
});
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'porimport { RestEndpoint } from '@data-client/rest' - Converte
axios.create({ baseURL, headers })em uma subclasse base deRestEndpointcomurlPrefixegetHeaders() - Transforma
axios.get(),.post(),.put(),.patch(),.delete()emnew RestEndpoint({ path, method }) - Transforma chamadas em uma instância criada (
api.post(), em queapi = axios.create(...)) emnew 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:
- Interceptors — veja métodos de ciclo de vida
- Tratamento de erros (
isAxiosError,error.response) — veja tratamento de erros - O restante da referência rápida — veja os exemplos de migração acima
- Definições de schema de Entity e a conversão dos pontos de chamada para hooks — veja abaixo
Encontrando usos restantes do axios
Padrões de busca para localizar o que ainda precisa ser migrado:
| Padrão | Encontra |
|---|---|
import.*from ['"]axios['"] | instruções de import |
axios\.create | criação de instância |
axios\.(get|post|put|patch|delete) | chamadas diretas |
\.interceptors\.(request|response)\.use | interceptors |
isAxiosError | tratamento de erros |
cancelToken|CancelToken | cancelamento (obsoleto) |
onUploadProgress|onDownloadProgress | callbacks 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 emprocess()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
schemaindefinido 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).
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
- Before (axios)
- After (data-client)
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>;
}
import { useSuspense } from '@data-client/react';
import { UserResource } from './UserResource';
function UserProfile({ id }: { id: string }) {
const user = useSuspense(UserResource.get, { id });
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.
Guias relacionados
- Autenticação — padrões de autenticação por token e cookie
- Abortando o fetch — cancelamento e debouncing
- Transformando dados no fetch — transformações de resposta, renomeação de campos, downloads de arquivos
- Integração com Django — CSRF e autenticação por cookie para Django