Migrating from Axios
@data-client/rest replaces axios with a declarative, type-safe approach to REST APIs.
AI-assisted migration
Install the REST setup skill to automate the migration with your AI coding assistant. It auto-detects axios in your project and runs the codemod for deterministic transforms, then guides you through the manual steps that require judgment (interceptors, error handling, schema definitions, etc.).
- Skills
- OpenSkills
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 --skill data-client-schema --skill data-client-rest-setup --skill data-client-rest
Then run skill /data-client-rest-setup to start the migration. It will detect axios and apply the appropriate migration sub-procedure automatically.
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 boilerplate —
resource()generates a full CRUD API from apathandschema
Quick reference
| Axios | @data-client/rest |
|---|---|
baseURL | urlPrefix |
headers config | getHeaders() |
interceptors.request | getRequestInit() / getHeaders() |
interceptors.response | parseResponse() / process() |
timeout | AbortSignal.timeout() via signal |
params / paramsSerializer | searchParams / searchToString() |
cancelToken / signal | signal (AbortController) |
responseType: 'blob' | Custom parseResponse() — see file download |
auth: { username, password } | getHeaders() with btoa() |
transformRequest | getRequestInit() |
transformResponse | process() |
validateStatus | Custom fetchResponse() |
onUploadProgress | Custom fetchResponse() with ReadableStream |
isAxiosError / error.response | NetworkError with .status and .response |
Migration examples
Basic GET
- 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",
"email": "[email protected]"
}
Instance with base URL and 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"
}
POST mutation
- 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 → lifecycle methods
Axios interceptors map to RestEndpoint lifecycle methods:
- 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 already returns parsed JSON by default — no interceptor needed to unwrap response.data.
Error handling
- 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 provides .status and .response (the raw Response object). For soft retries on server errors, see errorPolicy.
Cancellation
- Before (axios)
- After (data-client)
import axios from 'axios';
const controller = new AbortController();
axios.get('/users', { signal: controller.signal });
controller.abort();
The useCancelling() hook automatically cancels in-flight requests when parameters change:
import { useSuspense } from '@data-client/react';
import { useCancelling } from '@data-client/react';
function SearchResults({ query }: { query: string }) {
const results = useSuspense(
useCancelling(searchEndpoint),
{ q: query },
);
return <ResultsList results={results} />;
}
For manual cancellation, pass signal directly:
const controller = new AbortController();
const getUser = new RestEndpoint({
path: '/users/:id',
signal: controller.signal,
});
controller.abort();
See the abort guide for more patterns.
Timeout
axios.get('/users', { timeout: 5000 });
const getUsers = new RestEndpoint({
path: '/users',
signal: AbortSignal.timeout(5000),
});
Codemod
For non-AI workflows, a standalone jscodeshift codemod handles the mechanical parts of migration. (The AI skill above runs this automatically as its first step.)
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'withimport { RestEndpoint } from '@data-client/rest' - Converts
axios.create({ baseURL, headers })into a baseRestEndpointclass - Transforms
axios.get(),.post(),.put(),.patch(),.delete()intoRestEndpointinstances
After running the codemod, you'll need to manually:
- Define Entity schemas for your response data
- Convert imperative
api.get()call sites to declarativeuseSuspense()hooks - Migrate interceptors to lifecycle methods (see examples above)
- Set up resource() for CRUD endpoints
Related guides
- Authentication — token and cookie auth patterns
- Aborting Fetch — cancellation and debouncing
- Transforming data on fetch — response transforms, field renaming, file downloads
- Django Integration — CSRF and cookie auth for Django