Migrar desde Axios
@data-client/rest reemplaza axios con un enfoque declarativo y con tipado seguro para las APIs REST.
Migración asistida por IA
Instala el skill de configuración de REST para automatizar la migración con tu asistente de programación con IA. Detecta automáticamente axios en tu proyecto y ejecuta el codemod para las transformaciones deterministas; después te guía por los pasos manuales que requieren criterio (interceptores, manejo de errores, definiciones de schemas, 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
Luego ejecuta el skill /data-client-rest-setup para iniciar la migración. Detectará axios y aplicará automáticamente el subprocedimiento de migración adecuado.
¿Por qué migrar?
Rutas con tipado seguro
Con axios, las rutas de la API son cadenas opacas: los errores tipográficos y los parámetros que faltan solo se detectan en tiempo de ejecución:
// axios: no type checking — typo silently produces wrong URL
axios.get(`/users/${usrId}`);
Con RestEndpoint, los parámetros de la ruta se infieren de la plantilla path y se exigen en tiempo de compilación:
const getUser = new RestEndpoint({ path: '/users/:id', schema: User });
// TypeScript enforces { id: string } — typos are compile errors
getUser({ id: '1' });
Esto también significa que el autocompletado del IDE funciona para cada parámetro de la ruta.
Ventajas adicionales
- Caché normalizada — las entidades compartidas se deduplican y se actualizan automáticamente en todas partes
- Dependencias de datos declarativas — los componentes declaran qué datos necesitan mediante
useSuspense(), no cómo obtenerlos - Actualizaciones optimistas — respuesta instantánea en la interfaz antes de que responda el servidor
- Cero código repetitivo —
resource()genera una API CRUD completa a partir de unpathy unschema
Referencia rápida
| Axios | @data-client/rest |
|---|---|
baseURL | urlPrefix |
configuración headers | getHeaders() |
interceptors.request | getRequestInit() / getHeaders() |
interceptors.response | parseResponse() / process() |
timeout | AbortSignal.timeout() mediante signal |
params / paramsSerializer | searchParams / searchToString() |
cancelToken / signal | signal (AbortController) |
responseType: 'blob' / 'arraybuffer' | content: 'blob' / 'arrayBuffer' — consulta la descarga de archivos |
auth: { username, password } | getHeaders() con btoa() |
xsrfCookieName / xsrfHeaderName | getHeaders() — consulta la integración con Django |
transformRequest | getRequestInit() |
transformResponse | process() |
validateStatus | fetchResponse() personalizado |
onUploadProgress | fetchResponse() personalizado con XMLHttpRequest |
isAxiosError / error.response | NetworkError con .status y .response |
Ejemplos de migración
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",
}
Instancia con URL base y cabeceras
- 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"
}
Mutación 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"
}
Interceptores → métodos del ciclo de vida
Los interceptores de axios se corresponden con los métodos del ciclo de vida de 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;
}
}
RestEndpoint ya devuelve el JSON analizado de forma predeterminada; no hace falta ningún interceptor para extraer response.data.
Los interceptores de respuesta que transforman el cuerpo, como convertir claves snake_case, pertenecen a process(). Consulta de snake a camel para ver un ejemplo completo.
Manejo de errores
- 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 proporciona .status y .response (el objeto Response sin procesar). Para reintentos suaves ante errores del servidor, consulta errorPolicy.
Mensajes de error del servidor
Es habitual que las bases de código con axios muestren al usuario error.response.data.error o .message. En su lugar, léelo una sola vez del cuerpo de la Response, en el fetchResponse() de la clase base, de modo que los puntos de llamada lo obtengan de error.message sin analizar el cuerpo:
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;
}
}
}
Cancelación
- Before (axios)
- After (data-client)
import axios from 'axios';
const controller = new AbortController();
axios.get('/users', { signal: controller.signal });
controller.abort();
O con el CancelToken, ya obsoleto:
const source = axios.CancelToken.source();
axios.get('/users', { cancelToken: source.token });
source.cancel();
Ambos se corresponden con un signal de AbortController. El hook useCancelling() cancela automáticamente las peticiones en curso cuando cambian los parámetros:
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 la cancelación manual, pasa signal directamente:
const controller = new AbortController();
const getUser = new RestEndpoint({
path: '/users/:id',
signal: controller.signal,
});
controller.abort();
Consulta la guía de abort para ver más patrones.
Tiempo de espera
axios.get('/users', { timeout: 5000 });
const getUsers = new RestEndpoint({
path: '/users',
signal: AbortSignal.timeout(5000),
});
Respuestas binarias
axios.get('/files/1', { responseType: 'blob' });
Establece content en 'blob', 'arrayBuffer' o 'text'. Consulta la descarga de archivos para ver el endpoint completo y cómo iniciar una descarga en el navegador.
Serialización de la consulta
axios.get('/users', {
params: { ids: [1, 2, 3] },
paramsSerializer: params =>
qs.stringify(params, { arrayFormat: 'repeat' }),
});
Sobrescribe searchToString() para serializar con qs; consulta el uso de la librería qs.
Autenticación 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')}`,
};
}
}
Aceptar estados de error
fetchResponse() lanza NetworkError para cualquier estado que no sea ok. Sobrescríbelo para cambiar qué se considera un error:
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;
}
}
Cabeceras CSRF
axios.create({
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken',
});
Lee la cookie en getHeaders() para las peticiones que no sean GET. Consulta la integración con Django para ver la clase de endpoint completa.
Progreso de subida
fetch no puede informar del progreso de subida, así que usa XMLHttpRequest dentro de fetchResponse(). El campo onProgress se pasa como una opción del endpoint, como cualquier otro miembro.
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
Un codemod independiente de jscodeshift se encarga de las partes mecánicas de la migración. Ejecútalo tú mismo si no usas flujos de trabajo con IA; el skill de IA de arriba lo ejecuta automáticamente como primer paso.
npx jscodeshift -t https://dataclient.io/codemods/axios-to-rest.js --extensions=ts,tsx,js,jsx src/
El codemod, de forma automática:
- Reemplaza
import axios from 'axios'porimport { RestEndpoint } from '@data-client/rest' - Convierte
axios.create({ baseURL, headers })en una subclase base deRestEndpointconurlPrefixygetHeaders() - Transforma
axios.get(),.post(),.put(),.patch(),.delete()ennew RestEndpoint({ path, method }) - Transforma las llamadas sobre una instancia creada (
api.post()dondeapi = axios.create(...)) ennew CreatedClassName({ path, method })
El codemod tiene poco que hacer cuando el proyecto envuelve axios en su propia clase o función y nunca llama a axios.get()/.post() directamente, o solo llama a axios(config) sin nombre de método. En esos casos, sáltalo y empieza con los pasos manuales.
El codemod no se encarga de:
- Los interceptores — consulta los métodos del ciclo de vida
- El manejo de errores (
isAxiosError,error.response) — consulta el manejo de errores - El resto de la referencia rápida — consulta los ejemplos de migración de arriba
- Las definiciones de schemas de Entity y la conversión de los puntos de llamada a hooks — consulta más abajo
Encontrar el uso restante de axios
Patrones de búsqueda para localizar lo que aún falta por migrar:
| Patrón | Encuentra |
|---|---|
import.*from ['"]axios['"] | sentencias import |
axios\.create | creación de instancias |
axios\.(get|post|put|patch|delete) | llamadas directas |
\.interceptors\.(request|response)\.use | interceptores |
isAxiosError | manejo de errores |
cancelToken|CancelToken | cancelación (obsoleta) |
onUploadProgress|onDownloadProgress | callbacks de progreso |
Después del codemod
El codemod produce endpoints sin schemas. Definir schemas de Entity y conectarlos a los endpoints habilita la normalización y el almacenamiento en caché, que es el valor central de Reactive Data Client.
Claves primarias no estándar
Muchas APIs (MongoDB, por ejemplo) usan _id en lugar de id. Sobrescribe pk():
import { Entity } from '@data-client/rest';
export class User extends Entity {
_id = '';
name = '';
email = '';
static key = 'User';
pk() {
return this._id;
}
}
Agrupar endpoints CRUD con resource()
Cuando un módulo de axios tiene funciones separadas getUsers, getUser, createUser, updateUser y deleteUser para una misma ruta, reemplázalas por un ú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
Las rutas anidadas como /projects/:projectId/tasks/:taskId tienen su propio resource. Reserva new ApiEndpoint() independiente para las operaciones que no son CRUD (búsqueda, acciones personalizadas, autenticación).
Convivir con Zod o Yup
Si la base de código ya valida las respuestas con Zod o Yup, elige un enfoque por tipo:
-
Zod en
process()(recomendado): mantén la validación en tiempo de ejecución analizando enprocess()y deja que la Entity se encargue de la normalización:const getUser = new ApiEndpoint({path: '/users/:id',schema: User,process(value: any) {return userSchema.parse(value);},}); -
La Entity reemplaza a Zod: mueve la forma de los campos a la clase Entity y elimina el schema de Zod. Los campos de la Entity aportan tipos, no comprobaciones en tiempo de ejecución, así que añade
static validate()para cualquier campo que el servidor pueda enviar mal formado. -
Solo Zod, sin Entity: deja
schemasin definir y analiza manualmente. Hazlo solo en los endpoints que no se benefician de la normalización (tokens de autenticación, respuestas puntuales).
No definas clases Entity y luego dejes schema sin definir en todos los endpoints: sin schema, nada se normaliza y la migración aporta poco respecto a axios.
Tipado del cuerpo
Tipa el cuerpo de los endpoints POST/PUT/PATCH independientes con body: {} as BodyType. No uses undefined as unknown as BodyType: RestEndpoint trata body: undefined como si no hubiera argumento de cuerpo.
const createUser = new ApiEndpoint({
path: '/users',
method: 'POST',
body: {} as { name: string; email: string },
schema: User,
});
resource() tipa sus endpoints CRUD automáticamente.
Convertir los puntos de llamada a 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>;
}
Los estados de carga y de error pasan a AsyncBoundary. Consulta useSuspense() para más detalles.
Autenticación basada en contexto
Cuando los tokens provienen del contexto de React (Okta, Auth0) en lugar del almacenamiento, usa hookifyResource() para inyectar las cabeceras mediante un hook. Consulta la guía de autenticación para este y otros patrones.
Migración gradual
Si la aplicación usa TanStack Query o SWR y no puede convertirlo todo de una vez, conserva temporalmente esos hooks pero haz el fetch a través de controller.fetch(). Llamar a un endpoint directamente solo ejecuta su fetch; pasar por el Controller también normaliza la respuesta en la caché compartida, de modo que los datos son consistentes desde el primer día:
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 }),
});
}
Más adelante, reemplaza useProject(id) por useSuspense(getProject, { id }).
Abstracciones de endpoint existentes
Las bases de código que ya tienen una clase de endpoint personalizada que envuelve axios (por ejemplo, una con path, method y un ayudante toDynamicUrl()) pueden extender RestEndpoint en lugar de reemplazarla, manteniendo los métodos retrocompatibles y ganando url(), getRequestInit(), fetchResponse() and 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,
});
Pasa los miembros adicionales como queryKey como opciones en lugar de mediante un constructor personalizado, para que extend() (usado por resource(), hookifyResource() y useCancelling()) siga funcionando.
Guías relacionadas
- Autenticación — patrones de autenticación con tokens y cookies
- Cancelar el fetch — cancelación y debouncing
- Transformar datos al hacer fetch — transformaciones de respuestas, renombrado de campos, descargas de archivos
- Integración con Django — CSRF y autenticación por cookies para Django