Skip to main content

Migrating from Axios

@data-client/rest replaces axios with a declarative, type-safe approach to REST APIs.

Why migrate?

Type-safe paths

With axios, API paths are opaque strings — typos and missing parameters are only caught at runtime:

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

With RestEndpoint, path parameters are inferred from the path template and enforced at compile time:

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

This also means IDE autocomplete works for every path parameter.

Additional benefits

  • Normalized cache — shared entities are deduplicated and updated everywhere automatically
  • Declarative data dependencies — components declare what data they need via useSuspense(), not how to fetch it
  • Optimistic updates — instant UI feedback before the server responds
  • Zero boilerplateresource() generates a full CRUD API from a path and schema

Quick reference

Axios@data-client/rest
baseURLurlPrefix
headers configgetHeaders()
interceptors.requestgetRequestInit() / getHeaders()
interceptors.responseparseResponse() / process()
timeoutAbortSignal.timeout() via signal
params / paramsSerializersearchParams / searchToString()
cancelToken / signalsignal (AbortController)
responseType: 'blob'Custom parseResponse() — see file download
auth: { username, password }getHeaders() with btoa()
transformRequestgetRequestInit()
transformResponseprocess()
validateStatusCustom fetchResponse()
onUploadProgressCustom fetchResponse() with ReadableStream
isAxiosError / error.responseNetworkError with .status and .response

Migration examples

Basic GET

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

Instance with base URL and 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);

POST mutation

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 → lifecycle methods

Axios interceptors map to RestEndpoint lifecycle methods:

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

RestEndpoint already returns parsed JSON by default — no interceptor needed to unwrap response.data.

Error handling

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

Cancellation

import axios from 'axios';

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

See the abort guide for more patterns.

Timeout

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

Automated codemod

A jscodeshift codemod handles the mechanical parts of migration:

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

The codemod automatically:

  • Replaces import axios from 'axios' with import { RestEndpoint } from '@data-client/rest'
  • Converts axios.create({ baseURL, headers }) into a base RestEndpoint class
  • Transforms axios.get(), .post(), .put(), .patch(), .delete() into RestEndpoint instances

After running the codemod, you'll need to manually:

  • Define Entity schemas for your response data
  • Convert imperative api.get() call sites to declarative useSuspense() hooks
  • Migrate interceptors to lifecycle methods (see examples above)
  • Set up resource() for CRUD endpoints