restfit
v1.0.11
Published
A TypeScript decorator-based REST API client library
Maintainers
Readme
RestFit
A TypeScript decorator-based REST API client library that provides a clean and type-safe way to define API services.
Features
- 🎯 Type-safe: Full TypeScript support with type inference
- 🎨 Decorator-based: Clean, declarative API using decorators
- 🔧 Flexible: Support for path parameters, query strings, headers, and request bodies
- ⚡ Error Handling: Custom error and success handlers for different HTTP status codes
- 🛡️ Resilient: Built-in retry policies and circuit breaker patterns (similar to Refit)
- 📦 Lightweight: Built on top of axios with minimal overhead
Installation
npm install restfit axios reflect-metadata axios-retry axios-circuit-breakerQuick Start
import 'reflect-metadata';
import { Get, Post, Path, Query, Body, Header, createApiService } from 'restfit';
class UserService {
@Get('/users/{userId}')
async getUser(@Path('userId') userId: string): Promise<User> {
// Implementation is handled by the decorator
}
@Post('/users')
async createUser(@Body() user: User): Promise<User> {
// Implementation is handled by the decorator
}
@Get('/users')
async listUsers(
@Query('page') page: number,
@Query('limit') limit: number,
@Header('Authorization') auth: string
): Promise<User[]> {
// Implementation is handled by the decorator
}
}
// Create an instance
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
headers: {
'Content-Type': 'application/json'
},
// Optional: Authorization
authorization: 'your-token-here', // Static token
// Or use a function for dynamic tokens:
// authorization: async () => await getTokenFromStorage(),
authorizationType: 'Bearer' // 'Bearer' | 'Basic' | 'Custom'
});
// Use it
const user = await userService.getUser('123');
const newUser = await userService.createUser({ name: 'John' });
// Or create multiple services at once
const api = createApiService(
{
baseUrl: 'https://api.example.com',
headers: { 'Content-Type': 'application/json' }
},
{
users: UserService,
posts: PostService,
comments: CommentService
}
);
// Use them
const user = await api.users.getUser('123');
const posts = await api.posts.listPosts();HTTP Method Decorators
@Get(path)- GET request@Post(path)- POST request@Put(path)- PUT request@Patch(path)- PATCH request@Delete(path)- DELETE request
Parameter Decorators
@Path(name)- Path parameter (e.g.,/users/{userId})@Query(name)- Query parameter (e.g.,?page=1)@Body()- Request body@Header(name)- Request header@SerializedName(name)or@AliasAs(name)- Map property name to different JSON key (for request/response serialization)
Serialization Decorators
Use @SerializedName or @AliasAs to map TypeScript property names to different JSON keys:
class User {
@SerializedName('first_name')
firstName: string;
@SerializedName('last_name')
lastName: string;
@AliasAs('email_address') // Alternative name
email: string;
}Handler Decorators
@OnError(handler)- Catch-all error handler (catches all errors regardless of status)- Example:
@OnError((error) => { /* handle all errors */ })
- Example:
@OnError(status, handler)- Custom error handler for specific status codesstatuscan be a single number, array of numbers, ornullfor catch-all- Example:
@OnError([404, 500], handler)or@OnError(null, handler)
@OnSuccess(status, handler)- Custom success handler for specific status codesstatuscan be a single number or array of numbers- Example:
@OnSuccess([200, 201], handler)
@OnRetrying(handler)- Custom retry handler that is called when a request is being retried- Handler receives
retryCount(number) anderror(AxiosError) - Example:
@OnRetrying((retryCount, error) => { console.log(Retry ${retryCount}); })
- Handler receives
Example: Using @OnRetrying
import { Get, Path, OnRetrying, createApiService } from 'restfit';
import { AxiosError } from 'axios';
class UserService {
@Get('/users/{id}')
@OnRetrying((retryCount, error) => {
console.log(`🔄 Retry attempt ${retryCount} for user ${error.config?.url}`);
if (error.response) {
console.log(` Status: ${error.response.status}`);
}
// You can also trigger custom actions like:
// - Update retry metrics
// - Send notifications
// - Adjust retry strategy
})
async getUser(@Path('id') id: number): Promise<User> {
return {} as User;
}
}Note: The @OnRetrying decorator works in conjunction with the resilience retry policy. If retries are disabled in the resilience configuration, the handler will not be called.
Response Interceptors
RestFit supports response interceptors that allow you to check responses, trigger actions, and optionally modify responses.
There are two ways to use interceptors:
- Global interceptors - Applied to all requests via
ApiServiceConfig - Method-specific interceptors - Applied via
@ResponseInterceptordecorator
Both can be used together, with global interceptors running first.
Interceptors can:
- Return
voidfor side effects (logging, triggering actions, etc.) - Return a modified
AxiosResponseto transform the response data
Global Response Interceptors
Configure interceptors at the service level to apply to all requests:
import { createApiService, ResponseInterceptorConfig } from 'restfit';
import { AxiosResponse } from 'axios';
const globalInterceptors: ResponseInterceptorConfig[] = [
{
// Handler: Check condition and execute action in one function
handler: (response) => {
if (response.headers['x-rate-limit-remaining']) {
const remaining = response.headers['x-rate-limit-remaining'];
const limit = response.headers['x-rate-limit-limit'];
console.log(`Rate limit: ${remaining}/${limit}`);
if (Number(remaining) < 10) {
notificationService.warn('Rate limit is getting low!');
}
}
}
},
{
// Handler: Check for custom header and update app state
handler: async (response) => {
if (response.headers['x-something']) {
const customValue = response.headers['x-something'];
await appState.update({ customFlag: customValue });
}
}
}
];
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
responseInterceptors: globalInterceptors
});Method-Specific Interceptors (Decorator)
Use @ResponseInterceptor decorator for method-specific interceptors:
import { Get, ResponseInterceptor } from 'restfit';
import { AxiosResponse } from 'axios';
class UserService {
@Get('/users')
@ResponseInterceptor((response) => {
// Side effect: Check condition and execute action (void return)
if (response.data.length > 0) {
console.log(`Retrieved ${response.data.length} users`);
}
})
@ResponseInterceptor(async (response) => {
// Side effect: Multiple interceptors can be chained
// Check for specific header
if (response.headers['x-custom']) {
const value = response.headers['x-custom'];
eventBus.emit('custom-event', { value });
}
})
async getUsers(): Promise<User[]> {
return [];
}
@Get('/users/{id}')
@ResponseInterceptor((response) => {
// Modify response: Transform the response data
if (response.data) {
response.data = {
...response.data,
metadata: {
fetchedAt: new Date().toISOString(),
processed: true
}
};
return response; // Return modified response
}
})
async getUser(@Path('id') id: number): Promise<User & { metadata?: any }> {
return {} as User & { metadata?: any };
}
}Use Cases:
- Monitor rate limit headers and trigger warnings
- Check for custom headers and update app state
- Analyze response data and trigger notifications
- Track analytics based on response characteristics
- Update cache strategies based on cache-control headers
- Global logging or monitoring across all API calls
- Transform response data before it reaches your code
- Add computed properties or metadata to responses
- Normalize API responses to match your data models
Automatic Response Wrapping
RestFit can automatically wrap all responses in a consistent format, similar to Microsoft's API client pattern. This eliminates the need for try-catch blocks and provides a consistent response structure.
Enable response wrapping via the wrapResponses config option:
import { Get, createApiService } from 'restfit';
class UserService {
@Get('/users')
async getUsers(): Promise<User[]> {
return [];
}
@Get('/users/{id}')
async getUser(@Path('id') id: number): Promise<User> {
return {} as User;
}
}
// Enable automatic response wrapping
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
wrapResponses: true,
});
// Usage - no try-catch needed!
const { data: users, success, error } = await userService.getUsers();
if (success) {
console.log('Users:', users);
} else {
console.error('Error:', error?.message);
}Response Format:
When wrapResponses: true:
- Success (object): The original response object is spread with
success: trueadded- Example:
{ users: [...] }becomes{ users: [...], success: true }
- Example:
- Success (array/primitive): Wrapped in
{ data: T, success: true } - Error:
{ success: false, error: <@OnError handler return value> }
The error field contains whatever the @OnError handler returns. If the handler doesn't return anything, error will be undefined.
Benefits:
- ✅ No try-catch blocks needed
- ✅ Consistent response structure across all methods
- ✅ Type-safe error handling
- ✅ Works seamlessly with existing code (method signatures remain unchanged)
Note: When wrapResponses is enabled, errors are returned as wrapped responses instead of being thrown, but only for methods that have @OnError handlers. Methods without error handlers will still throw errors even when wrapResponses: true.
Authorization Configuration
RestFit supports flexible authorization configuration:
// Static Bearer token
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
authorization: 'your-token-here',
authorizationType: 'Bearer'
});
// Dynamic token (function)
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
authorization: async () => {
// Get token from storage, refresh if needed, etc.
return await getTokenFromStorage();
},
authorizationType: 'Bearer'
});
// Basic authentication
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
authorization: Buffer.from('username:password').toString('base64'),
authorizationType: 'Basic'
});
// Custom authorization header
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
authorization: 'CustomTokenValue',
authorizationType: 'Custom'
});Resilience Features
RestFit includes built-in resilience features similar to Refit in .NET, with automatic retry policies and circuit breaker patterns enabled by default.
Default Resilience Policy
By default, RestFit applies a resilience policy that includes:
- Retry Policy: 3 retries with exponential backoff (100ms initial, max 2000ms)
- Circuit Breaker: Opens after 5 failures in 1 minute, resets after 30 seconds
- Retries on: 408, 429, 500, 502, 503, 504 status codes and network errors
Using Default Resilience
// Default resilience is automatically applied
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com'
});Customizing Resilience
import { createApiService, DEFAULT_RESILIENCE_POLICY } from 'restfit';
// Custom resilience configuration
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
resilience: {
retry: {
retries: 5,
retryDelay: 200,
retryDelayMax: 5000,
exponentialBackoff: true,
retryableStatusCodes: [429, 500, 502, 503, 504],
retryOnNetworkError: true
},
circuitBreaker: {
enabled: true,
threshold: 10,
window: 60000,
timeout: 60000,
minimumRequests: 5,
errorStatusCodes: [500, 502, 503, 504]
}
}
});Disabling Resilience
// Disable all resilience features
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
resilience: false
});
// Disable only retry
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
resilience: {
retry: false,
circuitBreaker: {
// custom circuit breaker config
}
}
});Advanced Resilience Configuration
// Custom retry logic
const userService = createApiService(UserService, {
baseUrl: 'https://api.example.com',
resilience: {
retry: {
retries: 3,
shouldRetry: async (error, retryCount) => {
// Custom logic to determine if request should be retried
if (retryCount >= 3) return false;
if (error.response?.status === 429) return true;
return false;
},
retryDelayFn: (retryCount, error) => {
// Custom delay calculation
if (error.response?.status === 429) {
return error.response.headers['retry-after'] || 1000;
}
return 100 * Math.pow(2, retryCount);
}
},
circuitBreaker: {
isFailure: (error) => {
// Custom failure detection
return error.response?.status >= 500 || !error.response;
}
}
}
});Resilience Policy Options
RetryPolicy
retries(number): Number of retry attempts (default: 3)retryDelay(number): Initial delay in milliseconds (default: 100)retryDelayMax(number): Maximum delay in milliseconds (default: 2000)exponentialBackoff(boolean | number): Enable exponential backoff with optional multiplier (default: true)retryableStatusCodes(number[]): HTTP status codes that trigger retry (default: [408, 429, 500, 502, 503, 504])retryOnNetworkError(boolean): Retry on network errors (default: true)shouldRetry(function): Custom function to determine if request should be retriedretryDelayFn(function): Custom function to calculate retry delay
CircuitBreakerPolicy
enabled(boolean): Enable circuit breaker (default: true)threshold(number): Failure threshold before opening circuit (default: 5)window(number): Time window in milliseconds to count failures (default: 60000)timeout(number): Time in milliseconds before attempting to close circuit (default: 30000)minimumRequests(number): Minimum requests in window before circuit can open (default: 10)errorStatusCodes(number[]): HTTP status codes that count as failures (default: [500, 502, 503, 504])isFailure(function): Custom function to determine if an error counts as a failure
Credits
RestFit is inspired by:
- Retrofit - A type-safe HTTP client for Android and Java by Square
- Refit - The automatic type-safe REST library for .NET by ReactiveUI
We thank the creators and maintainers of these excellent libraries for their inspiration and design patterns.
License
MIT
