
Security News
Feross on TBPN: How North Korea Hijacked Axios
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.
@appello/services
Advanced tools
A comprehensive TypeScript library providing reusable service utilities for handling API requests, GraphQL operations, and data fetching across web and mobile React applications.
npm install @appello/services
Peer Dependencies:
npm install @appello/common
import { createApiService, createGqlClient } from '@appello/services';
// REST API Service
const apiService = createApiService({
url: 'https://api.example.com',
getAccessToken: () => localStorage.getItem('accessToken'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
onTokenRefreshSuccess: tokens => {
localStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
},
onTokenRefreshError: () => {
// Handle logout
},
refreshTokenUrl: '/auth/refresh',
});
// GraphQL Client
const gqlClient = createGqlClient({
url: 'https://api.example.com/graphql',
wsUrl: 'wss://api.example.com/graphql',
getAccessToken: () => localStorage.getItem('accessToken'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
onTokenRefreshSuccess: tokens => {
localStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
},
onTokenRefreshError: () => {
// Handle logout
},
refreshTokens: async (client, context) => {
// Custom refresh logic
},
});
import { createProcessApiErrorResponse } from '@appello/services';
const processApiError = createProcessApiErrorResponse({
onGlobalError: message => toast.error(message),
onUnknownErrors: message => console.error(message),
});
| Service | Purpose | Factory Function |
|---|---|---|
| --------------- | ---------------------------------------- | --------------------------- |
| REST API | HTTP requests with auth & error handling | createApiService(config) |
| GraphQL | Apollo Client with subscriptions & auth | createGqlClient(config) |
| React Query | TanStack React Query client | createQueryClient(config) |
import { createApi } from '@reduxjs/toolkit/query/react';
import { axiosBaseQuery, handleRtkQueryError } from '@appello/services';
export const api = createApi({
baseQuery: axiosBaseQuery({ api: apiService }),
endpoints: builder => ({
login: builder.mutation({
query: credentials => ({
url: '/auth/login',
method: 'POST',
data: credentials,
}),
}),
}),
});
// Usage
const [login] = api.useLoginMutation();
try {
await login(credentials).unwrap();
} catch (error) {
processError({ errors: handleRtkQueryError(error) });
}
import { useMutation } from '@tanstack/react-query';
import { handleApiRequestError } from '@appello/services';
const mutation = useMutation({
mutationFn: credentials => apiService.post('/auth/login', credentials),
onError: error => {
processError({ errors: handleApiRequestError({ error }) });
},
});
import { useForm } from 'react-hook-form';
const { handleSubmit, setError } = useForm();
const onSubmit = async data => {
try {
await apiService.post('/auth/login', data);
} catch (error) {
processError({
errors: handleApiRequestError({ error }),
fields: ['username', 'password'],
setFormError: setError,
});
}
};
import { createGqlClient } from '@appello/services';
const gqlClient = createGqlClient({
url: 'https://api.example.com/graphql',
wsUrl: 'wss://api.example.com/graphql',
getAccessToken: () => localStorage.getItem('accessToken'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
onTokenRefreshSuccess: tokens => {
localStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
},
onTokenRefreshError: () => {
window.location.href = '/login';
},
refreshTokens: async (client, context) => {
const { data } = await client.mutate({
mutation: REFRESH_TOKEN_MUTATION,
context,
});
return data.refreshToken;
},
});
| Function | Purpose | Use Case |
|---|---|---|
handleApiRequestError({ error }) | Process REST API errors | Convert Axios errors to user-friendly format |
handleRtkQueryError(error) | Process RTK Query errors | Extract error data from RTK Query responses |
createProcessApiErrorResponse(config) | Form error processor | Integrate API errors with React Hook Form |
createProcessGqlErrorResponse(config) | GraphQL error processor | Handle GraphQL errors in forms |
Adds Bearer token to request headers without mutating the original headers object.
import { setAuthorizationHeader } from '@appello/services';
const headers = { 'Content-Type': 'application/json' };
const token = 'my-jwt-token';
const newHeaders = setAuthorizationHeader(token, headers);
// Result: { 'Content-Type': 'application/json', Authorization: 'Bearer my-jwt-token' }
Parameters:
token: string - The access tokenheaders: AxiosRequestHeaders - Existing headers objectReturns: AxiosRequestHeaders - New headers object with Authorization header
Creates an authorization header object for GraphQL requests.
import { getGqlAuthorizationHeader } from '@appello/services';
const authHeader = getGqlAuthorizationHeader('my-jwt-token');
// Result: { Authorization: 'Bearer my-jwt-token' }
Parameters:
token: string - The access tokenReturns: { Authorization: string } - Authorization header object
Handles JWT token refresh for expired authentication tokens with sophisticated queuing and retry mechanisms.
// This function is typically used internally by the API service
// but can be customized through ApiServiceConfig
Purpose: Implements a comprehensive token refresh mechanism that prevents race conditions by ensuring only one refresh operation runs at a time, while queuing additional requests until the refresh completes.
Features:
Configuration: Configured through ApiServiceConfig in the API service setup.
Extracts error data from RTK Query responses and converts them to a standardized format.
import { handleRtkQueryError } from '@appello/services';
try {
await login(credentials).unwrap();
} catch (error) {
const errors = handleRtkQueryError(error);
processError({ errors });
}
Parameters:
error: unknown - RTK Query error objectReturns: ResponseErrors - Standardized error format
Features:
ResponseErrors formatProcesses Axios API errors into a standardized ResponseErrors format.
import { handleApiRequestError } from '@appello/services';
try {
await apiService.post('/api/endpoint', data);
} catch (error) {
const errors = handleApiRequestError({ error });
// errors: { field1: 'Error message', field2: 'Another error' }
}
Parameters:
error: AxiosError - The Axios error objecthandleDetailError?: (error: ResponseErrors) => ResponseErrors - Custom field error handleronManualHandleError?: (error: AxiosError) => ResponseErrors - Override error handlingReturns: ResponseErrors - Map of field names to error messages
Features:
Creates a processor for integrating API errors with React Hook Form.
import { createProcessApiErrorResponse } from '@appello/services';
const processError = createProcessApiErrorResponse({
onGlobalError: message => toast.error(message),
onUnknownErrors: message => console.error(message),
});
// Usage with form
const { setError } = useForm();
try {
await apiService.post('/api/login', credentials);
} catch (error) {
processError({
errors: handleApiRequestError({ error }),
fields: ['username', 'password'],
setFormError: setError,
});
}
Configuration Options:
onGlobalError?: (message: string) => void - Handle non-field errorsonUnknownErrors?: (message: string) => void - Handle unknown errorsonUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void - Handle unhandled fieldsUsage Parameters:
errors: ResponseErrors - Error object from handleApiRequestErrorfields: string[] | Record<string, string> - Form fields to handlesetFormError?: (name: string, error: { message: string }) => void - React Hook Form setErrorCreates a processor for handling GraphQL errors in forms.
import { createProcessGqlErrorResponse } from '@appello/services';
const processGqlError = createProcessGqlErrorResponse({
onNonFieldError: message => toast.error(message),
onUnknownError: message => console.error(message),
});
// Usage with GraphQL errors
const { setError } = useForm();
try {
await apolloClient.mutate({ mutation: LOGIN_MUTATION, variables: credentials });
} catch (gqlError) {
processGqlError(gqlError, {
fields: ['username', 'password'],
setFormError: setError,
});
}
Configuration Options:
onNonFieldError?: (message: string) => void - Handle general errorsonUnknownError?: (message: string) => void - Handle unknown errorsonUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void - Handle unhandled fieldsUsage Parameters:
gqlError: unknown - GraphQL error objectfields: string[] | Record<string, string> - Form fields to handlesetFormError?: (name: string, error: { message: string }) => void - React Hook Form setErrorimport {
getGqlErrors,
getGqlError,
isGqlUnauthorizedError,
isGqlBusinessError,
isGqlUnknownError,
} from '@appello/services';
// Extract errors from GraphQL response
const errors = getGqlErrors(gqlError);
// Get first error
const firstError = getGqlError(errors);
// Check error types
if (isGqlUnauthorizedError(errors)) {
// Handle 401 unauthorized
}
if (isGqlBusinessError(errors)) {
// Handle business logic errors
}
interface ApiServiceConfig<T = ApiBaseTokens> {
url: string; // Base API URL
getAccessToken?: () => Promise<string | null>; // Access token getter
getRefreshToken?: () => Promise<string | null>; // Refresh token getter
onTokenRefreshSuccess: (tokens: T) => void; // Success callback
onTokenRefreshError: (error?: unknown) => void; // Error callback
refreshTokenUrl?: string; // Refresh endpoint
refreshTokens?: (instance, error) => Promise<T>; // Custom refresh logic
getAccessTokenFromRefreshRequest?: (data) => string; // Extract token from response
axiosConfig?: CreateAxiosDefaults; // Axios configuration
}
interface GqlClientConfig<T = GqlBaseTokens> {
url: string; // GraphQL endpoint
wsUrl?: string; // WebSocket URL for subscriptions
getAccessToken: () => Promise<string | null>; // Access token getter
getRefreshToken: () => Promise<string | null>; // Refresh token getter
onTokenRefreshSuccess: (tokens: T) => void; // Success callback
onTokenRefreshError: (error?: unknown) => void; // Error callback
refreshTokens: (client, context) => Promise<T>; // Custom refresh logic
refreshTokenOperationName?: string; // Refresh mutation name
refreshTokenErrorMessage?: string; // Error message
onUnknownError?: (message: string) => void; // Unknown error handler
cache?: InMemoryCacheConfig; // Apollo cache config
additionalLinks?: ApolloLink | ApolloLink[]; // Extra Apollo links
uploadLinkParams?: object; // File upload config
}
import {
createApiService,
createGqlClient,
createProcessApiErrorResponse,
handleApiRequestError,
} from '@appello/services';
import { toast } from 'react-toastify';
// Tokens management
const getAccessToken = () => localStorage.getItem('accessToken');
const getRefreshToken = () => localStorage.getItem('refreshToken');
const handleTokenRefreshSuccess = tokens => {
localStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
};
const handleTokenRefreshError = () => {
localStorage.clear();
window.location.href = '/login';
};
// API Service
export const apiService = createApiService({
url: process.env.REACT_APP_API_URL,
getAccessToken,
getRefreshToken,
onTokenRefreshSuccess: handleTokenRefreshSuccess,
onTokenRefreshError: handleTokenRefreshError,
refreshTokenUrl: '/auth/refresh',
});
// GraphQL Client
export const gqlClient = createGqlClient({
url: `${process.env.REACT_APP_API_URL}/graphql`,
wsUrl: `${process.env.REACT_APP_WS_URL}/graphql`,
getAccessToken,
getRefreshToken,
onTokenRefreshSuccess: handleTokenRefreshSuccess,
onTokenRefreshError: handleTokenRefreshError,
refreshTokens: async (client, context) => {
const { data } = await client.mutate({
mutation: REFRESH_TOKEN_MUTATION,
context,
});
return data.refreshToken;
},
});
// Error Processing
export const processApiError = createProcessApiErrorResponse({
onGlobalError: message => toast.error(message),
onUnknownErrors: message => {
console.error('Unknown API error:', message);
toast.error('An unexpected error occurred');
},
});
// Usage in components
export const useLogin = () => {
const { setError } = useForm();
return useMutation({
mutationFn: credentials => apiService.post('/auth/login', credentials),
onError: error => {
processApiError({
errors: handleApiRequestError({ error }),
fields: ['email', 'password'],
setFormError: setError,
});
},
});
};
Token Refresh Not Working
refreshTokenUrl is correctgetRefreshToken returns valid tokenonTokenRefreshSuccess saves tokens properlyGraphQL Subscriptions Failing
wsUrl is accessibleForm Errors Not Showing
setFormError function is passed correctlyonUnhandledFieldErrors to debug unmapped fieldsRTK Query Integration Issues
axiosBaseQuery receives initialized API serviceEnable detailed logging:
const apiService = createApiService({
// ... config
axiosConfig: {
// Enable request/response logging
transformRequest: [
(data, headers) => {
console.log('API Request:', { data, headers });
return data;
},
],
},
});
Package Version: 4.0.1
License: ISC
Author: Appello Software
For more examples and advanced usage, check the test files in the repository.
FAQs
Services package with api / graphql
We found that @appello/services demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.

Security News
OpenSSF has issued a high-severity advisory warning open source developers of an active Slack-based campaign using impersonation to deliver malware.

Research
/Security News
Malicious packages published to npm, PyPI, Go Modules, crates.io, and Packagist impersonate developer tooling to fetch staged malware, steal credentials and wallets, and enable remote access.