@ticatec/restful_service_api
v0.8.0
Published
A lightweight TypeScript RESTful API client for browsers with error handling. (ESM only, v0.6.0+)
Readme
@ticatec/restful_service_api
A lightweight TypeScript RESTful API client for browsers with comprehensive error handling and interceptor support.
中文版 | English
Features
- 🚀 Lightweight: Zero dependencies, built for modern browsers
- 🔧 TypeScript Support: Full type safety with TypeScript definitions
- 🛡️ Error Handling: Built-in error handling with custom ApiError class
- ⚡ Interceptors: Pre and post request interceptors for authentication and data processing
- 🎯 Flexible: Support for custom headers, timeouts, and data processors
- 📁 File Operations: Dedicated interface for file upload/download operations
- 🌐 Browser-First: Designed specifically for frontend applications
- ✨ PATCH Support: Full support for HTTP PATCH method for partial updates
⚠️ Breaking Changes in v0.8.0
ErrorHandler has been changed to return a Promise (async support).
What This Means for You
- The
ErrorHandlerfunction type is now(ex: Error) => Promise<boolean | void>. - Error handler functions should now be
asyncfunctions or return aPromise. UploadCallback.handleErrornow adopts the unified Promise-basedErrorHandlertype.
Migration Guide
Before (v0.7.x and earlier):
const errorHandler: ErrorHandler = (error: Error) => {
console.error('API Error:', error);
return true;
};After (v0.8.0+):
const errorHandler: ErrorHandler = async (error: Error) => {
console.error('API Error:', error);
return true;
};⚠️ Breaking Changes in v0.7.0
RestfulOptions.contentType has been removed in favor of a general-purpose headers field.
What This Means for You
contentTypeis no longer a recognized field onRestfulOptions(used bypost/put/del/patch). Replace{ contentType: 'multipart/form-data' }with{ headers: { 'Content-Type': 'multipart/form-data' } }.get()gained a new, optional 4th parameter:get(url, params?, dataProcessor?, headers?). Existing positional calls (get(url),get(url, params),get(url, params, dataProcessor)) are unaffected.- This also enables custom per-call headers in general (not just Content-Type), e.g. a conditional
GET via
If-None-Match— see Custom Headers (e.g. Conditional Requests).
Migration Guide
Before (v0.6.x and earlier):
await api.post('/upload', formData, { contentType: 'multipart/form-data' });After (v0.7.0+):
await api.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } });⚠️ Breaking Changes in v0.5.0
Starting from version 0.5.0, this package has migrated to ESM (ECMAScript Modules) format.
What This Means for You
- Your project must use ESM format (have
"type": "module"in package.json or use.mjsextension) require()is no longer supported. Useimportstatements instead
Migration Guide
If you're upgrading from a version prior to 0.5.0:
Before (CommonJS):
const RestService = require('@ticatec/restful_service_api');After (ESM):
import RestService from '@ticatec/restful_service_api';For CommonJS projects, you may need to use dynamic import:
const { default: RestService } = await import('@ticatec/restful_service_api');Installation
npm install @ticatec/restful_service_apiQuick Start
import RestService, { FileService } from '@ticatec/restful_service_api';
// Your implementation of RestService interface
class MyApiClient implements RestService {
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>) {
// Implementation here
}
async post(url: string, data?: any, options?: RestfulOptions) {
// Implementation here
}
async patch(url: string, data?: any, options?: RestfulOptions) {
// Implementation here
}
// ... other methods
}
// Your implementation of FileService interface
class MyFileClient implements FileService {
async upload(url: string, params: any, file: File, fileKey?: string, dataProcessor?: DataProcessor) {
// Implementation here
}
async asyncUpload(url: string, params: any, file: File, callback: UploadCallback, fileKey?: string) {
// Implementation here
}
async download(url: string, filename: string, params: any, method?: string, formData?: any) {
// Implementation here
}
}
const api = new MyApiClient();
const fileApi = new MyFileClient();
// Make REST requests
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John Doe' });
const partialUpdate = await api.patch('/users/1', { status: 'active' });
// File operations
const result = await fileApi.upload('/upload', { userId: 123 }, file);API Reference
RestService Interface
The main interface for standard HTTP REST operations:
interface RestService {
get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any>;
post(url: string, data?: any, options?: RestfulOptions): Promise<any>;
put(url: string, data?: any, options?: RestfulOptions): Promise<any>;
patch(url: string, data?: any, options?: RestfulOptions): Promise<any>;
del(url: string, data?: any, options?: RestfulOptions): Promise<any>;
}FileService Interface
Dedicated interface for file upload and download operations:
interface FileService {
upload(url: string, params: any, file: File, fileKey?: string, dataProcessor?: DataProcessor): Promise<any>;
asyncUpload(url: string, params: any, file: File, callback: UploadCallback, fileKey?: string): Promise<UploadProgress>;
download(url: string, filename: string, params: any, method?: string, formData?: any): Promise<any>;
}RestService Methods
get(url, params?, dataProcessor?, headers?)
Performs HTTP GET request to retrieve a resource.
- url: The endpoint URL
- params: Query parameters (optional)
- dataProcessor: Function to process response data (optional)
- headers: Custom request headers for this single call (optional), e.g. for a conditional
request:
{ 'If-None-Match': contentHash }
post(url, data?, options?)
Performs HTTP POST request to create a new resource.
- url: The endpoint URL
- data: Request payload (optional)
- options: Optional configuration object containing:
- params: Query parameters (optional)
- headers: Custom request headers for this single call (optional). Use this to override
the default
Content-Type(application/json), e.g.{ 'Content-Type': 'multipart/form-data' } - dataProcessor: Function to process response data (optional)
put(url, data?, options?)
Performs HTTP PUT request to update an entire resource.
- url: The endpoint URL
- data: Request payload (optional)
- options: Optional configuration object containing:
- params: Query parameters (optional)
- headers: Custom request headers for this single call (optional)
- dataProcessor: Function to process response data (optional)
patch(url, data?, options?)
Performs HTTP PATCH request to partially update a resource.
- url: The endpoint URL
- data: Request payload with fields to update (optional)
- options: Optional configuration object containing:
- params: Query parameters (optional)
- headers: Custom request headers for this single call (optional)
- dataProcessor: Function to process response data (optional)
del(url, data?, options?)
Performs HTTP DELETE request to delete a resource.
- url: The endpoint URL
- data: Request body data (optional)
- options: Optional configuration object containing:
- params: Query parameters (optional)
- headers: Custom request headers for this single call (optional)
- dataProcessor: Function to process response data (optional)
FileService Methods
upload(url, params, file, fileKey?, dataProcessor?)
Performs synchronous file upload operation.
- url: The upload endpoint URL
- params: Additional parameters for the upload request
- file: The File object to upload
- fileKey: Optional form field name for the file (defaults to 'file')
- dataProcessor: Function to process response data (optional)
asyncUpload(url, params, file, callback, fileKey?)
Performs asynchronous file upload with progress monitoring and cancellation support.
- url: The upload endpoint URL
- params: Additional parameters for the upload request
- file: The File object to upload
- callback: Upload callback object with progress, error, and completion handlers
- fileKey: Optional form field name for the file (defaults to 'file')
- Returns: Promise that resolves to UploadProgress object for cancellation
download(url, filename, params, method?, formData?)
Performs file download operation.
- url: The download endpoint URL
- filename: The name to save the downloaded file
- params: Download request parameters
- method: Optional HTTP method (defaults to GET)
- formData: Optional form data for POST downloads
Error Handling
The library includes a custom ApiError class for handling API errors:
import { ApiError } from '@ticatec/restful_service_api';
try {
const result = await api.get('/users');
} catch (error) {
if (error instanceof ApiError) {
console.log('Status:', error.status);
console.log('Code:', error.code);
console.log('Details:', error.details);
}
}Types and Interceptors
import {
PreInterceptor,
PostInterceptor,
ErrorHandler,
DataProcessor,
RestfulOptions
} from '@ticatec/restful_service_api';
// RestfulOptions for configuring requests
const options: RestfulOptions = {
params: { page: 1, limit: 10 },
headers: { 'Content-Type': 'application/json' },
dataProcessor: (data: any) => data.results || data
};
// Pre-interceptor for adding authentication headers
const preInterceptor: PreInterceptor = (method: string, url: string) => {
return {
headers: {
'Authorization': 'Bearer ' + getToken(),
'X-Request-ID': generateRequestId()
},
timeout: 30000
};
};
// Post-interceptor for response processing
const postInterceptor: PostInterceptor = async (data: any) => {
// Process response data
return data;
};
// Error handler
const errorHandler: ErrorHandler = async (error: Error) => {
console.error('API Error:', error);
return true; // Return true if error is handled
};
// Data processor
const dataProcessor: DataProcessor = (data: any) => {
// Transform response data
return data.results || data;
};Content Types
Pre-defined content type constants:
import {
CONTENT_TYPE_NAME,
TYPE_JSON,
TYPE_HTML,
TYPE_TEXT
} from '@ticatec/restful_service_api';
// Usage - set a header explicitly via RestfulOptions.headers
await api.post('/upload', data, { headers: { [CONTENT_TYPE_NAME]: TYPE_JSON } });Custom Headers (e.g. Conditional Requests)
RestfulOptions.headers (and the 4th headers parameter of get()) let you attach custom
request headers to a single call. A common use case is a conditional GET based on a
previously-cached ETag/hash:
// First request: no cached hash yet
const pack = await api.get('/messages/node1');
localStorage.setItem('node1-hash', pack.contentHash);
// Later: send the cached hash as If-None-Match
const cachedHash = localStorage.getItem('node1-hash');
try {
const updated = await api.get('/messages/node1', undefined, undefined, {
'If-None-Match': cachedHash ?? ''
});
// Server responded 200 - content changed, use `updated`
} catch (error) {
if (error instanceof ApiError && Number(error.status) === 304) {
// Server responded 304 Not Modified - the local cache is still valid
} else {
throw error;
}
}Implementation Example
Here's a complete example implementation using the native fetch API:
RestService Implementation
import RestService, {
ApiError,
PreInterceptor,
PostInterceptor,
RestfulOptions,
TYPE_JSON,
DataProcessor
} from '@ticatec/restful_service_api';
class FetchRestService implements RestService {
private baseURL: string;
private preInterceptor?: PreInterceptor;
private postInterceptor?: PostInterceptor;
constructor(baseURL: string, preInterceptor?: PreInterceptor, postInterceptor?: PostInterceptor) {
this.baseURL = baseURL;
this.preInterceptor = preInterceptor;
this.postInterceptor = postInterceptor;
}
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any> {
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request('GET', url + queryString, null, headers, dataProcessor);
}
async post(url: string, data?: any, options?: RestfulOptions): Promise<any> {
const params = options?.params;
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request('POST', url + queryString, data, options?.headers, options?.dataProcessor);
}
async put(url: string, data?: any, options?: RestfulOptions): Promise<any> {
const params = options?.params;
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request('PUT', url + queryString, data, options?.headers, options?.dataProcessor);
}
async patch(url: string, data?: any, options?: RestfulOptions): Promise<any> {
const params = options?.params;
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request('PATCH', url + queryString, data, options?.headers, options?.dataProcessor);
}
async del(url: string, data?: any, options?: RestfulOptions): Promise<any> {
const params = options?.params;
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request('DELETE', url + queryString, data, options?.headers, options?.dataProcessor);
}
private async request(method: string, url: string, body?: any, customHeaders?: Record<string, string>, dataProcessor?: DataProcessor): Promise<any> {
const fullUrl = this.baseURL + url;
// Apply pre-interceptor
const interceptorResult = this.preInterceptor?.(method, fullUrl) || { headers: {} };
const headers = {
'Content-Type': TYPE_JSON,
...interceptorResult.headers,
...customHeaders
};
try {
const response = await fetch(fullUrl, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: interceptorResult.timeout ? AbortSignal.timeout(interceptorResult.timeout) : undefined
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(response.status, errorData);
}
let responseData = await response.json();
// Apply post-interceptor
if (this.postInterceptor) {
responseData = await this.postInterceptor(responseData);
}
// Apply data processor
if (dataProcessor) {
responseData = dataProcessor(responseData);
}
return responseData;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(0, { code: 'NETWORK_ERROR', message: error.message });
}
}
}
// Usage
const api = new FetchRestService('https://api.example.com');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John Doe' });
const partialUpdate = await api.patch('/users/1', { status: 'active' });
const fullUpdate = await api.put('/users/1', { name: 'Jane Doe', age: 30 });FileService Implementation
import FileService, { UploadCallback, UploadProgress, DataProcessor } from '@ticatec/restful_service_api';
class FetchFileService implements FileService {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async upload(url: string, params: any, file: File, fileKey?: string, dataProcessor?: DataProcessor): Promise<any> {
const formData = new FormData();
formData.append(fileKey || 'file', file);
// Add additional parameters
Object.keys(params).forEach(key => {
formData.append(key, params[key]);
});
const response = await fetch(this.baseURL + url, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
return dataProcessor ? dataProcessor(data) : data;
}
async asyncUpload(url: string, params: any, file: File, callback: UploadCallback, fileKey?: string): Promise<UploadProgress> {
const formData = new FormData();
formData.append(fileKey || 'file', file);
Object.keys(params).forEach(key => {
formData.append(key, params[key]);
});
const xhr = new XMLHttpRequest();
return new Promise((resolve) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable && callback.progressUpdate) {
callback.progressUpdate(e.loaded);
}
});
xhr.addEventListener('load', async () => {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
callback.onCompleted(data);
} else if (callback.handleError) {
await callback.handleError(new Error(`Upload failed: ${xhr.statusText}`));
}
});
xhr.addEventListener('error', async () => {
if (callback.handleError) {
await callback.handleError(new Error('Network error'));
}
});
xhr.open(callback.method || 'POST', this.baseURL + url);
xhr.send(formData);
resolve({
cancel: () => xhr.abort()
});
});
}
async download(url: string, filename: string, params: any, method?: string, formData?: any): Promise<any> {
let fullUrl = this.baseURL + url;
if (method === 'POST' && formData) {
const form = new FormData();
Object.keys(formData).forEach(key => {
form.append(key, formData[key]);
});
const response = await fetch(fullUrl, {
method: 'POST',
body: form
});
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
a.click();
window.URL.revokeObjectURL(downloadUrl);
} else {
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
fullUrl += queryString;
const response = await fetch(fullUrl);
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
a.click();
window.URL.revokeObjectURL(downloadUrl);
}
}
}
// Usage
const fileApi = new FetchFileService('https://api.example.com');
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files[0];
// Simple upload
const result = await fileApi.upload('/upload', { userId: 123 }, file);
// Async upload with progress
const progress = await fileApi.asyncUpload('/upload', { userId: 123 }, file, {
method: 'POST',
progressUpdate: (loaded) => console.log(`Uploaded: ${loaded} bytes`),
onCompleted: (data) => console.log('Done!', data),
handleError: async (err) => console.error('Error!', err)
});
// Download file
await fileApi.download('/files/document.pdf', 'my-document.pdf', { userId: 123 });Utility Functions
The library includes utility functions for common operations:
import utils from '@ticatec/restful_service_api/utils';
// Convert object to query string
const queryString = utils.toQueryString({ name: 'John', age: 30 });
// Returns: "name=John&age=30"
// Combine URL with parameters
const fullUrl = utils.combineUrl('/api/users', { page: 1, limit: 10 });
// Returns: "/api/users?page=1&limit=10"
// Generate HTTP request options
const options = utils.generateRequestOptions('POST', { id: 1 }, { name: 'John' });
// Returns: { method: 'POST', headers: {}, params: { id: 1 }, data: { name: 'John' } }
// Clean parameters (removes null, undefined, empty strings, trims values)
const cleanedParams = utils.cleanParams({ name: ' John ', age: null, email: '' });
// Returns: { name: 'John' }
// Function utilities
utils.invokeFunction(callback, arg1, arg2); // Safely invoke function if it exists
const isFunc = utils.isFunction(someValue); // Check if value is a functionAvailable Utility Functions
toQueryString(obj): Converts an object to URL query stringcombineUrl(url, params): Combines URL with query parametersgenerateRequestOptions(method, params, data): Generates HTTP request optionscleanParams(params): Cleans parameters by removing null/empty values and trimming stringsisFunction(value): Checks if a value is a functioninvokeFunction(func, ...args): Safely invokes a function if it exists
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see the LICENSE file for details.
Author
Henry Feng
For more examples and advanced usage, please check the documentation.
