Migrating from Axios
@data-client/rest replaces axios with a declarative, type-safe approach to REST APIs.
AI-assisted migration
Install the axios-to-rest-migration skill to automate the migration with your AI coding assistant. The skill 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 axios-to-rest-migration
npx openskills install reactive/data-client --skill axios-to-rest-migration
Browse all skills on skills.sh
Then run skill /axios-to-rest-migration to start the migration.
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": "alice@example.com"
}
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