A robust TypeScript API service framework for making authenticated API calls with advanced features:
- âś… Multiple authentication strategies (token, API key, basic auth, custom)
- âś… Request caching with configurable time periods
- âś… Advanced retry mechanisms with exponential backoff
- âś… Status code-specific hooks for handling errors
- âś… Account state tracking
- âś… File upload support
- âś… Support for multiple accounts or a single default account
- âś… Automatic token refresh for 401 errors (if supported by provider)
npm install @rendomnet/apiservice
ApiService includes a comprehensive test suite using Jest. To run the tests:
# Run tests
npm test
# Run tests with coverage report
npm run test:coverage
# Run tests in watch mode during development
npm run test:watch
import ApiService from 'apiservice';
import { TokenAuthProvider, ApiKeyAuthProvider, BasicAuthProvider } from 'apiservice';
// Token-based (OAuth2, etc.)
const tokenProvider = new TokenAuthProvider(myTokenService);
// API key in header
const apiKeyHeaderProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', headerName: 'x-api-key' });
// API key in query param
const apiKeyQueryProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', queryParamName: 'api_key' });
// Basic Auth
const basicProvider = new BasicAuthProvider({ username: 'user', password: 'pass' });
// Create and setup the API service
const api = new ApiService();
api.setup({
provider: 'my-service',
authProvider: tokenProvider, // or apiKeyHeaderProvider, apiKeyQueryProvider, basicProvider
hooks: {
// You can define custom hooks here,
// or use the default token refresh handler for 401 errors (if supported)
},
cacheTime: 30000, // 30 seconds
baseUrl: 'https://api.example.com' // Set default base URL
});
// Make API calls with specific account ID and use default baseUrl
const result = await api.call({
accountId: 'user123',
method: 'GET',
route: '/users',
useAuth: true
});
// Override default baseUrl for specific calls
const customResult = await api.call({
method: 'GET',
base: 'https://api2.example.com', // Override default baseUrl
route: '/users',
useAuth: true
});
// Or omit accountId to use the default account ('default')
const defaultResult = await api.call({
method: 'GET',
route: '/users',
useAuth: true
});
ApiService supports multiple authentication strategies via the AuthProvider
interface. You can use built-in providers or implement your own.
import { TokenAuthProvider } from 'apiservice';
const tokenService = {
async get(accountId = 'default') {
// Get token from storage
return storedToken;
},
async set(token, accountId = 'default') {
// Save token to storage
},
async refresh(refreshToken, accountId = 'default') {
// Refresh the token with your OAuth provider
// ...
return newToken;
}
};
const tokenProvider = new TokenAuthProvider(tokenService);
import { ApiKeyAuthProvider } from 'apiservice';
// API key in header
const apiKeyHeaderProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', headerName: 'x-api-key' });
// API key in query param
const apiKeyQueryProvider = new ApiKeyAuthProvider({ apiKey: 'my-key', queryParamName: 'api_key' });
import { BasicAuthProvider } from 'apiservice';
const basicProvider = new BasicAuthProvider({ username: 'user', password: 'pass' });
You can implement your own provider by implementing the AuthProvider
interface:
interface AuthProvider {
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
refresh?(refreshToken: string, accountId?: string): Promise<any>;
}
If your provider supports token refresh (like TokenAuthProvider
), ApiService includes a built-in handler for 401 (Unauthorized) errors that automatically refreshes tokens. This feature:
- Detects 401 errors from the API
- Calls the provider's
refresh
method - Retries the original API request with the new token
To use this feature:
- Use a provider that implements
refresh
(likeTokenAuthProvider
) - Don't specify a custom 401 hook (the default will be used automatically)
If you prefer to handle token refresh yourself, you can either:
- Provide your own handler for 401 errors which will override the default
- Disable the default handler by setting
hooks: { 401: null }
api.setup({
provider: 'my-service',
authProvider: tokenProvider,
hooks: {
401: null // Explicitly disable the default handler
},
cacheTime: 30000
});
ApiService supports multiple accounts through the accountId
parameter. This allows you to:
- Manage multiple tokens - Maintain separate authentication tokens for different users or services
- Track state by account - Each account has its own state tracking (request times, failures)
- Apply account-specific retry logic - Hooks can behave differently based on the account
For simple applications that only need a single account, you can omit the accountId parameter:
// Make calls without specifying accountId - uses 'default' automatically
const result = await api.call({
method: 'GET',
route: '/users'
});
If no accountId is provided, ApiService automatically uses 'default' as the account ID.
interface AuthProvider {
getAuthHeaders(accountId?: string): Promise<Record<string, string>>;
refresh?(refreshToken: string, accountId?: string): Promise<any>;
}
import ApiService from 'apiservice';
import { TokenAuthProvider } from 'apiservice';
const tokenService = {
async get(accountId = 'default') {
// ...
},
async set(token, accountId = 'default') {
// ...
},
async refresh(refreshToken, accountId = 'default') {
// ...
}
};
const api = new ApiService();
api.setup({
provider: 'example-api',
authProvider: new TokenAuthProvider(tokenService),
cacheTime: 30000,
baseUrl: 'https://api.example.com',
hooks: {
403: {
shouldRetry: false,
handler: async (accountId, response) => {
// ...
return null;
}
}
}
});
// Use the API service
async function fetchUserData(userId) {
return await api.call({
method: 'GET',
route: `/users/${userId}`,
useAuth: true
});
}
Hooks can be configured to handle specific HTTP status codes:
const hooks = {
401: {
shouldRetry: true,
useRetryDelay: true,
maxRetries: 3,
preventConcurrentCalls: true,
handler: async (accountId, response) => {
// ...
return { /* updated parameters */ };
},
onMaxRetriesExceeded: async (accountId, error) => {
// ...
},
onHandlerError: async (accountId, error) => {
// ...
},
delayStrategy: {
calculate: (attempt, response) => 1000 * Math.pow(2, attempt - 1)
},
maxDelay: 30000
}
}
The codebase is built around a main ApiService
class that coordinates several component managers:
HttpClient
: Handles the actual HTTP request creation and executionCacheManager
: Implements data caching with customizable expiration timesRetryManager
: Manages retry logic with exponential backoff and other delay strategiesHookManager
: Provides a way to hook into specific status codes and handle themAccountManager
: Tracks account state and handles account-specific data
import { ApiService, TokenAuthProvider, ApiKeyAuthProvider } from 'apiservice';
const primaryProvider = new TokenAuthProvider(primaryTokenService);
const secondaryProvider = new ApiKeyAuthProvider({ apiKey: 'secondary-key', headerName: 'x-api-key' });
const api = new ApiService();
api.setup({
provider: 'primary-api',
authProvider: primaryProvider,
cacheTime: 30000,
baseUrl: 'https://api.primary.com'
});
api.setup({
provider: 'secondary-api',
authProvider: secondaryProvider,
cacheTime: 60000,
baseUrl: 'https://api.secondary.com'
});
// Use different providers in API calls
async function fetchCombinedData() {
const [primaryData, secondaryData] = await Promise.all([
api.call({
provider: 'primary-api',
method: 'GET',
route: '/data',
useAuth: true
}),
api.call({
provider: 'secondary-api',
method: 'GET',
route: '/data',
useAuth: true
}),
api.call({
provider: 'primary-api',
method: 'GET',
route: '/special-data',
useAuth: true,
base: 'https://special-api.primary.com'
})
]);
return { primaryData, secondaryData };
}