FetchClient is a library that makes it easier to use the fetch API for JSON APIs. It provides the following features:
- Typed Response - Full TypeScript support with strongly typed responses
- Functional - Standalone functions for simple usage
- Model Validator - Built-in validation with Problem Details support
- Caching - Response caching with TTL and programmatic invalidation
- Middleware - Extensible middleware pipeline for request/response handling
- Rate Limiting - Built-in rate limiting with per-domain support
- Request Timeout - Configurable timeouts with AbortSignal support
- Error Handling - Comprehensive error handling with Problem Details
- Authentication - Built-in Bearer token support
- Base URL - Global base URL configuration
- Loading State - Track request loading state with events
npm install --save @exceptionless/fetchclient
import { FetchClient } from '@exceptionless/fetchclient';
type Products = {
products: Array<{ id: number; name: string }>;
};
const client = new FetchClient();
const response = await client.getJSON<Products>(
`https://dummyjson.com/products/search?q=iphone&limit=10`,
);
const products = response.data;
import { postJSON, getJSON } from '@exceptionless/fetchclient';
type Product = { id: number; title: string };
type Products = { products: Product[] };
const response = await postJSON<Product>(
"https://dummyjson.com/products/add",
{
name: "iPhone 13",
},
);
const product = await getJSON<Product>(
`https://dummyjson.com/products/${response.data!.id}`,
);
import { FetchClient, setModelValidator } from '@exceptionless/fetchclient';
setModelValidator(async (data: object | null) => {
// use zod or any other validator
const problem = new ProblemDetails();
const d = data as { password: string };
if (d?.password?.length < 6) {
problem.errors.password = [
"Password must be longer than or equal to 6 characters.",
];
}
return problem;
});
const client = new FetchClient();
const data = {
email: "test@test",
password: "test",
};
const response = await client.postJSON(
"https://jsonplaceholder.typicode.com/todos/1",
data,
);
if (!response.ok) {
// check response problem
console.log(response.problem.detail);
}
import { FetchClient } from '@exceptionless/fetchclient';
type Todo = { userId: number; id: number; title: string; completed: boolean };
const client = new FetchClient();
const response = await client.getJSON<Todo>(
`https://jsonplaceholder.typicode.com/todos/1`,
{
cacheKey: ["todos", "1"],
cacheDuration: 1000 * 60, // expires in 1 minute
}
);
// invalidate programmatically
client.cache.delete(["todos", "1"]);
import { FetchClient, useMiddleware } from '@exceptionless/fetchclient';
type Products = {
products: Array<{ id: number; name: string }>;
};
useMiddleware(async (ctx, next) => {
console.log('starting request')
await next();
console.log('completed request')
});
const client = new FetchClient();
const response = await client.getJSON<Products>(
`https://dummyjson.com/products/search?q=iphone&limit=10`,
);
import { FetchClient, useRateLimit } from '@exceptionless/fetchclient';
// Enable rate limiting globally with 100 requests per minute
useRateLimit({
maxRequests: 100,
windowSeconds: 60,
});
const client = new FetchClient();
const response = await client.getJSON(
`https://api.example.com/data`,
);
import { FetchClient } from '@exceptionless/fetchclient';
const client = new FetchClient();
// Set timeout for individual requests
const response = await client.getJSON(
`https://api.example.com/data`,
{ timeout: 5000 } // 5 seconds
);
// Use AbortSignal for cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
const response2 = await client.getJSON(
`https://api.example.com/data`,
{ signal: controller.signal }
);
import { FetchClient } from '@exceptionless/fetchclient';
const client = new FetchClient();
try {
const response = await client.getJSON(`https://api.example.com/data`);
} catch (error) {
// Handle HTTP errors (4xx, 5xx)
if (error.problem) {
console.log(error.problem.title);
console.log(error.problem.detail);
}
}
// Or handle specific status codes
const response = await client.getJSON(
`https://api.example.com/data`,
{
expectedStatusCodes: [404, 500],
errorCallback: (response) => {
if (response.status === 404) {
console.log('Resource not found');
return true; // Don't throw
}
}
}
);
import { FetchClient, setAccessTokenFunc } from '@exceptionless/fetchclient';
// Set global access token function
setAccessTokenFunc(() => localStorage.getItem('token'));
const client = new FetchClient();
const response = await client.getJSON(`https://api.example.com/data`);
// Automatically adds Authorization: Bearer <token> header
import { FetchClient, setBaseUrl } from '@exceptionless/fetchclient';
// Set global base URL
setBaseUrl('https://api.example.com');
const client = new FetchClient();
const response = await client.getJSON(`/users/123`);
// Requests to https://api.example.com/users/123
import { FetchClient } from '@exceptionless/fetchclient';
const client = new FetchClient();
// Track loading state
client.loading.on((isLoading) => {
console.log(`Loading: ${isLoading}`);
});
// Check current loading state
console.log(client.isLoading);
console.log(client.requestCount);
Also, take a look at the tests:
Run tests:
deno run test
Lint code:
deno lint
Format code:
deno fmt
Type check code:
deno run check
MIT © Exceptionless