npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@appello/services

v5.0.0

Published

Services package with api / graphql

Downloads

1,086

Readme

Frontend Services Library

A comprehensive TypeScript library providing reusable service utilities for handling API requests, GraphQL operations, and data fetching across web and mobile React applications.

📋 Table of Contents

✨ Features

  • 🌐 REST API Service - Axios-based with automatic token refresh
  • 🔗 GraphQL Client - Apollo Client with auth, error handling, and subscriptions
  • 🏪 RTK Query Integration - Redux Toolkit Query base query
  • 🔄 React Query Support - TanStack React Query client factory
  • 🔐 Authentication - JWT token management with refresh logic
  • ⚠️ Error Handling - Standardized error processing for forms
  • 🎯 TypeScript - Full type safety and IntelliSense support
  • 📱 Cross-Platform - Works with React web and React Native

📦 Installation

npm install @appello/services

Peer Dependencies:

npm install @appello/common

🚀 Quick Start

Basic Setup

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

Error Handling

import { createProcessApiErrorResponse } from '@appello/services';

const processApiError = createProcessApiErrorResponse({
  onGlobalError: message => toast.error(message),
  onUnknownErrors: message => console.error(message),
});

🏗️ Core Services

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

🔗 Integration Examples

🏪 RTK Query Integration

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

🔄 React Query Integration

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

🎯 Form Integration

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

🌐 GraphQL Setup

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

🛠️ Utilities

Error Handling Functions

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

Authentication Utilities

🔐 setAuthorizationHeader

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 token
  • headers: AxiosRequestHeaders - Existing headers object

Returns: AxiosRequestHeaders - New headers object with Authorization header

📊 getGqlAuthorizationHeader

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 token

Returns: { Authorization: string } - Authorization header object

Token Management

🔄 refreshTokens

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:

  • Race Condition Prevention: Only one refresh operation runs at a time
  • Request Queuing: Queues subsequent requests during refresh
  • Custom Refresh Logic: Supports both endpoint-based and custom refresh functions
  • Automatic Retry: Retries failed requests after successful token refresh
  • Error Handling: Proper cleanup on refresh failures

Configuration: Configured through ApiServiceConfig in the API service setup.

🏪 handleRtkQueryError

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 object

Returns: ResponseErrors - Standardized error format

Features:

  • RTK Query Integration: Specifically designed for RTK Query error handling
  • Error Type Detection: Handles different RTK Query error types
  • Standardized Output: Converts to consistent ResponseErrors format
  • Type Safety: Full TypeScript support

Form Error Processing

⚠️ handleApiRequestError

Processes 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 object
  • handleDetailError?: (error: ResponseErrors) => ResponseErrors - Custom field error handler
  • onManualHandleError?: (error: AxiosError) => ResponseErrors - Override error handling

Returns: ResponseErrors - Map of field names to error messages

Features:

  • Handles network, server, and validation errors
  • Flattens nested field errors to dot notation
  • Takes first error from arrays
  • Supports custom error processing

📝 createProcessApiErrorResponse

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 errors
  • onUnknownErrors?: (message: string) => void - Handle unknown errors
  • onUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void - Handle unhandled fields

Usage Parameters:

  • errors: ResponseErrors - Error object from handleApiRequestError
  • fields: string[] | Record<string, string> - Form fields to handle
  • setFormError?: (name: string, error: { message: string }) => void - React Hook Form setError

🔧 createProcessGqlErrorResponse

Creates 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 errors
  • onUnknownError?: (message: string) => void - Handle unknown errors
  • onUnhandledFieldErrors?: (errors: UnhandledFieldError[]) => void - Handle unhandled fields

Usage Parameters:

  • gqlError: unknown - GraphQL error object
  • fields: string[] | Record<string, string> - Form fields to handle
  • setFormError?: (name: string, error: { message: string }) => void - React Hook Form setError

GraphQL Error Helpers

🔍 GraphQL Error Detection

import {
  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
}

⚙️ Configuration

API Service Configuration

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
}

GraphQL Client 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
}

💡 Complete Example

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

❓ Troubleshooting

Common Issues

Token Refresh Not Working

  • Ensure refreshTokenUrl is correct
  • Check getRefreshToken returns valid token
  • Verify onTokenRefreshSuccess saves tokens properly

GraphQL Subscriptions Failing

  • Check wsUrl is accessible
  • Verify WebSocket connection in network tab
  • Ensure proper authentication context

Form Errors Not Showing

  • Check field names match between API and form
  • Verify setFormError function is passed correctly
  • Use onUnhandledFieldErrors to debug unmapped fields

RTK Query Integration Issues

  • Ensure axiosBaseQuery receives initialized API service
  • Check error handling in RTK Query endpoints
  • Verify error transformation logic

Debug Mode

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