Saltar al contenido principal

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.).

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

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 un path y un schema

Referencia rápida​

Axios@data-client/rest
baseURLurlPrefix
configuración headersgetHeaders()
interceptors.requestgetRequestInit() / getHeaders()
interceptors.responseparseResponse() / process()
timeoutAbortSignal.timeout() mediante signal
params / paramsSerializersearchParams / searchToString()
cancelToken / signalsignal (AbortController)
responseType: 'blob' / 'arraybuffer'content: 'blob' / 'arrayBuffer' — consulta la descarga de archivos
auth: { username, password }getHeaders() con btoa()
xsrfCookieName / xsrfHeaderNamegetHeaders() — consulta la integración con Django
transformRequestgetRequestInit()
transformResponseprocess()
validateStatusfetchResponse() personalizado
onUploadProgressfetchResponse() personalizado con XMLHttpRequest
isAxiosError / error.responseNetworkError con .status y .response

Ejemplos de migración​

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');

Instancia con URL base y cabeceras​

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);

Mutación 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);

Interceptores → métodos del ciclo de vida​

Los interceptores de axios se corresponden con los métodos del ciclo de vida de 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),
);
consejo

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​

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);
}
}

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:

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;
}
}
}

Cancelación​

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();

Consulta la guía de abort para ver más patrones.

Tiempo de espera​

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

Respuestas binarias​

Before (axios)
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​

Before (axios)
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​

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')}`,
};
}
}

Aceptar estados de error​

fetchResponse() lanza NetworkError para cualquier estado que no sea ok. Sobrescríbelo para cambiar qué se considera un error:

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;
}
}

Cabeceras CSRF​

Before (axios)
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.

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​

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' por import { RestEndpoint } from '@data-client/rest'
  • Convierte axios.create({ baseURL, headers }) en una subclase base de RestEndpoint con urlPrefix y getHeaders()
  • Transforma axios.get(), .post(), .put(), .patch(), .delete() en new RestEndpoint({ path, method })
  • Transforma las llamadas sobre una instancia creada (api.post() donde api = axios.create(...)) en new 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:

Encontrar el uso restante de axios​

Patrones de búsqueda para localizar lo que aún falta por migrar:

PatrónEncuentra
import.*from ['"]axios['"]sentencias import
axios\.createcreación de instancias
axios\.(get|post|put|patch|delete)llamadas directas
\.interceptors\.(request|response)\.useinterceptores
isAxiosErrormanejo de errores
cancelToken|CancelTokencancelación (obsoleta)
onUploadProgress|onDownloadProgresscallbacks 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 en process() 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 schema sin definir y analiza manualmente. Hazlo solo en los endpoints que no se benefician de la normalización (tokens de autenticación, respuestas puntuales).

aviso

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​

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>;
}

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.