Enhanced fetch wrapper with rate limiting, retries, and validation for Deno.
Note: This package is under active development. Contributions, feedback, and pull requests are welcome!
This package is designed to make the process of interacting with various APIs that have strict limitations more convenient and careful. For example, this could be APIs like Notion or Telegram, which have stringent limits.
- Gist deno telegram mailer + article đź’Ś Safe message sending script in Telegram with just 49 lines of code? Really?
deno add @vseplet/fetchifynpx jsr add @vseplet/fetchifyAnd import:
import fetchify from "@vseplet/fetchify";The first thing available to you is the fetchify function
const json = await (await fetchify("https://catfact.ninja/fact")).json();
console.log(json);This function has a similar interface to the classic fetch but extends it with additional options, for example:
const json = await (await fetchify(
"https://catfact.ninja/fact",
{
timeout: 1000, // Now, the waiting for a response will be interrupted after 1000 ms.
},
)).json();
console.log(json);But you can also create an instance with a set base URL and rate-limiting constraints:
const jph = fetchify.create({
limiter: {
// Number of requests per second
rps: 3,
// You can handle the occurrence of a 429 error
// and return the time in ms that the request loop should delay
rt: (response) => 1000,
},
baseURL: "https://jsonplaceholder.typicode.com",
headers: {
"hello": "world",
},
});
for (let i = 30; i--;) {
console.log(`send ${i}`);
// All basic methods supported: get post put delete head patch
jph.get(`/posts/${i}`).then((data) => console.log(`${i} ${data.status}`))
.catch((err) => console.log(`${i} ${err}`))
.finally(() => {
});
}Yes, all methods comply with the fetch interface but also extend it with additional options, for example:
await jph.get(`/posts/10`, {
// Number of attempts
attempts: 10
// Time after which we stop waiting for a response
timeout: 1000
});If you need to make a request to the configured baseURL but not through the request queue, you can add the flag:
await jph.get(`/posts/10`, { unlimited: true });If you need to, you can try to parse JSON and validate it using Zod:
import fetchify, { jsonZ, z } from "@vseplet/fetchify";
const schema = z.object({
id: z.string(), // there should actually be a z.number() here!
title: z.string(),
body: z.string(),
userId: z.number(),
});
const { data, response } = await jsonZ(
fetchify("https://jsonplaceholder.typicode.com/posts/1"),
schema,
);And get the error:
error: Uncaught (in promise) ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [
"id"
],
"message": "Expected string, received number"
}
]
Or using ValiBot:
import fetchify, { jsonV, v } from "@vseplet/fetchify";
const schema = v.object({
id: v.number(), // v.number() is valid
title: v.string(),
body: v.string(),
userId: v.number(),
});
const { data, response } = await jsonV(
fetchify("https://jsonplaceholder.typicode.com/posts/1"),
schema,
);
console.log(data);
// {
// id: 1,
// title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
// body: "quia et suscipit\n" +
// "suscipit recusandae consequuntur expedita et cum\n" +
// "reprehenderit molestiae ut ut quas"... 58 more characters,
// userId: 1
// }The main function for making HTTP requests with extended options.
Parameters:
input:string | URL | Request- The URL or Request objectinit?:ILimiterRequestInit- Optional request configuration
Options (extends RequestInit):
timeout?: number- Request timeout in millisecondsattempts?: number- Number of retry attempts on failureinterval?: number- Interval between retry attempts in millisecondsunlimited?: boolean- Bypass rate limiting for this requestparams?: IQueryParams- Query parameters to append to URL
Returns: Promise<Response>
Example:
import fetchify from "@vseplet/fetchify";
// Simple request
const response = await fetchify("https://api.example.com/data");
// With timeout
const response = await fetchify("https://api.example.com/data", {
timeout: 5000,
});
// With query params
const response = await fetchify("https://api.example.com/data", {
params: { page: 1, limit: 10 },
});
// With retries
const response = await fetchify("https://api.example.com/data", {
attempts: 3,
interval: 1000,
});Creates a configured instance with base URL, headers, and rate limiting.
Parameters:
config?:IFetchifyConfig- Configuration object
Configuration Options:
baseURL?: string | URL | Request- Base URL for all requestsheaders?: HeadersInit- Default headers for all requestslimiter?: ILimiterOptions- Rate limiting configurationrps?: number- Requests per second (default: 1)unlimited?: boolean- Disable rate limiting globallyrt?: (response: Response) => number- Rate limit handler, returns delay in msstatus?: { [code: number]: StatusHandler }- Custom handlers for specific status codes
Returns: Fetchify instance with methods: get, post, put, delete,
head, patch
Example:
import fetchify from "@vseplet/fetchify";
const api = fetchify.create({
baseURL: "https://api.example.com",
headers: {
"Authorization": "Bearer token",
"Content-Type": "application/json",
},
limiter: {
rps: 5, // 5 requests per second
rt: (response) => {
// Handle 429 (Too Many Requests)
const retryAfter = response.headers.get("Retry-After");
return retryAfter ? parseInt(retryAfter) * 1000 : 1000;
},
status: {
404: (response, resolve, reject, retry) => {
console.log("Resource not found");
reject(new Error("Not found"));
},
500: (response, resolve, reject, retry) => {
console.log("Server error, retrying...");
retry(); // Retry the request
},
},
},
});
// All HTTP methods available
await api.get("/users");
await api.post("/users", { body: JSON.stringify({ name: "John" }) });
await api.put("/users/1", { body: JSON.stringify({ name: "Jane" }) });
await api.patch("/users/1", { body: JSON.stringify({ age: 30 }) });
await api.delete("/users/1");
await api.head("/users");
// Bypass rate limiting for specific request
await api.get("/users", { unlimited: true });Helper functions for parsing and validating responses.
Parses response as plain text.
Parameters:
promise:Promise<Response>- Fetch promise
Returns: Promise<{ data: string, response: Response }>
Example:
import fetchify, { text } from "@vseplet/fetchify";
const { data, response } = await text(
fetchify("https://api.example.com/data.txt"),
);
console.log(data); // string content
console.log(response.status); // 200Parses response as JSON.
Parameters:
promise:Promise<Response>- Fetch promise
Returns: Promise<{ data: T, response: Response }>
Example:
import fetchify, { json } from "@vseplet/fetchify";
const { data, response } = await json(
fetchify("https://api.example.com/users"),
);
console.log(data); // parsed JSON objectParses response as JSON and validates with Zod schema.
Parameters:
promise:Promise<Response>- Fetch promiseschema:z.ZodSchema- Zod validation schema
Returns: Promise<{ data: T, response: Response }>
Throws: ZodError if validation fails
Example:
import fetchify, { jsonZ, z } from "@vseplet/fetchify";
const schema = z.object({
id: z.number(),
title: z.string(),
completed: z.boolean(),
});
const { data, response } = await jsonZ(
fetchify("https://jsonplaceholder.typicode.com/todos/1"),
schema,
);
console.log(data.id); // type-safe accessParses response as JSON and validates with ValiBot schema.
Parameters:
promise:Promise<Response>- Fetch promiseschema:v.BaseSchema- ValiBot validation schema
Returns: Promise<{ data: T, response: Response }>
Throws: ValiError if validation fails
Example:
import fetchify, { jsonV, v } from "@vseplet/fetchify";
const schema = v.object({
id: v.number(),
title: v.string(),
completed: v.boolean(),
});
const { data, response } = await jsonV(
fetchify("https://jsonplaceholder.typicode.com/todos/1"),
schema,
);
console.log(data.id); // type-safe accessAll types are exported from the package:
import type {
FetchInput,
IFetchifyConfig,
ILimiterOptions,
ILimiterRequestInit,
IQueryParams,
RateLimitExceededHandler,
StatusHandler,
} from "@vseplet/fetchify";// Input types for fetch
type FetchInput = URL | Request | string;
// Query parameters
interface IQueryParams {
[name: string]: string | number | boolean;
}
// Request options
interface ILimiterRequestInit extends RequestInit {
attempts?: number; // Number of retry attempts
interval?: number; // Interval between retries (ms)
timeout?: number; // Request timeout (ms)
unlimited?: boolean; // Bypass rate limiting
params?: IQueryParams; // Query parameters
}
// Rate limit handler
type RateLimitExceededHandler = (response: Response) => number;
// Status code handler
type StatusHandler = (
response: Response,
resolve: (value: Response) => void,
reject: (value: Error) => void,
retry: () => void,
) => void;
// Limiter configuration
interface ILimiterOptions {
unlimited?: boolean; // Disable rate limiting
rps?: number; // Requests per second
rt?: RateLimitExceededHandler; // Handle 429 errors
status?: { // Custom status handlers
[code: number]: StatusHandler;
};
}
// Fetchify configuration
interface IFetchifyConfig {
limiter?: ILimiterOptions; // Rate limiting config
baseURL?: FetchInput; // Base URL
headers?: HeadersInit; // Default headers
}