http-plz
v1.2.0
Published
A lightweight TypeScript fetch wrapper library that provides quality-of-life improvements including explicit path construction, type-safe query parameters, consistent error formatting, and automatic content handling.
Readme
http-plz
A lightweight TypeScript fetch wrapper library that provides quality-of-life improvements including explicit path construction, type-safe query parameters, consistent error formatting, and automatic content handling.
Features
- 🚀 Simple API: Clean and intuitive interface built on top of fetch
- 📝 TypeScript Support: Full TypeScript support with type safety
- 🎯 Multiple Response Types: Support for JSON, text, blob, arrayBuffer, and formData
- 🌊 Raw Response Streaming: Get the raw
ReadableStreamfor manual processing. - ⚡ Lightweight: Minimal dependencies and small bundle size
- 🔄 Request Configuration: Flexible request options and headers management
- 🔌 Middleware System: Powerful request and response interceptors with predictable execution order
Installation
npm install http-plzyarn add http-plzpnpm add http-plzQuick Start
import httpPlz from 'http-plz';
// Create a client instance
const api = httpPlz({
baseURL: 'https://jsonplaceholder.typicode.com',
options: {
headers: {
'Content-Type': 'application/json',
},
},
resolver: 'json', // Default response resolver
});
// Make requests
const users = await api.get({ path: '/users' });
const user = await api.post({
path: '/users',
body: { name: 'John Doe', email: '[email protected]' },
});API Reference
Configuration
interface Config {
baseURL: string;
options?: RequestInit;
resolver?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | null;
}Request Options
interface RequestOptions {
path: string;
query?: { [key: string]: string };
opts?: Omit<RequestInit, 'method' | 'body'>;
resolver?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | null;
body?: unknown;
}HTTP Methods
All methods return a Promise that resolves to an httpResponse<T> object:
// GET request
const response = await api.get<User[]>({
path: '/users',
query: { page: '1', limit: '10' },
});
// POST request
const response = await api.post<User>({
path: '/users',
body: { name: 'Jane Doe', email: '[email protected]' },
});
// PUT request
const response = await api.put<User>({
path: '/users/1',
body: { name: 'Jane Smith' },
});
// DELETE request
const response = await api.delete({
path: '/users/1',
});Usage Examples
Basic GET Request
import httpPlz from 'http-plz';
const api = httpPlz({
baseURL: 'https://api.example.com',
resolver: 'json', // Set a default resolver
});
const users = await api.get({
path: '/users',
});
console.log(users.data); // Response data
console.log(users.status); // HTTP status codeQuery Parameters
const users = await api.get({
path: '/users',
query: {
page: '1',
limit: '10',
sort: 'name',
},
});
// Requests: https://api.example.com/users?page=1&limit=10&sort=namePOST with Body
const newUser = await api.post({
path: '/users',
body: {
name: 'John Doe',
email: '[email protected]',
role: 'admin',
},
});Custom Headers and Options
const api = httpPlz({
baseURL: 'https://api.example.com',
options: {
headers: {
'Authorization': 'Bearer your-token',
'Content-Type': 'application/json',
},
},
});
// Override options per request
const response = await api.get({
path: '/protected-resource',
opts: {
headers: {
'X-Custom-Header': 'custom-value',
},
cache: 'no-cache',
},
});Different Response Types
The library can automatically parse the response body into different formats. You can specify the desired format using the resolver option in the client configuration or in a specific request.
// JSON response (default in this example)
const api = httpPlz({
baseURL: 'https://api.example.com',
resolver: 'json',
});
const jsonData = await api.get({
path: '/data.json', // Inherits 'json' resolver
});
// Text response (overriding the default)
const textData = await api.get({
path: '/data.txt',
resolver: 'text',
});
// Blob response (for files)
const fileBlob = await api.get({
path: '/file.pdf',
resolver: 'blob',
});Raw Response Streaming
For advanced use cases, like handling very large files or server-sent events, you can get the raw ReadableStream from the response.
1. Overriding a Default Resolver
If your client has a default resolver, you can override it by setting resolver: null in your request.
const api = httpPlz({
baseURL: 'https://api.example.com',
resolver: 'json', // Default resolver
});
// Get the raw stream by overriding the default
const streamResponse = await api.get({
path: '/stream',
resolver: null,
});
// The `data` property will be undefined
console.log(streamResponse.data); // undefined
// You can process the stream manually
if (streamResponse.body) {
const reader = streamResponse.body.getReader();
// ... process the stream
}2. No Default Resolver
If you don't set a default resolver on the client, requests will return a stream by default.
const streamApi = httpPlz({
baseURL: 'https://api.example.com',
// No default resolver
});
// This will return the raw stream
const response = await streamApi.get({ path: '/stream' });
// `response.data` is undefined, and you can use `response.body`Error Handling
try {
const response = await api.get({ path: '/users' });
console.log(response.data);
} catch (error) {
if (error instanceof HttpError) {
console.error('Request failed:', {
status: error.response.status,
statusText: error.response.statusText,
body: error.body,
requestOptions: error.requestOptions,
});
}
}Error Handling
The library provides structured error handling through the HttpError class. When a request fails (non-2xx status codes), the library formats the error and throws it for you to handle as needed.
HttpError Class
class HttpError extends Error {
name: string;
response: Response;
body: unknown;
requestOptions: RequestInit;
constructor(req: RequestInit, response: Response, body: unknown) {
super(`HTTP Error: ${response.status} ${response.statusText}`);
this.name = 'HttpError';
this.response = response;
this.body = body;
this.requestOptions = req;
}
}Error Properties
response: The original Response object from fetchbody: The error response body (parsed as JSON if possible, otherwise as text)requestOptions: The RequestInit options used for the requestmessage: Formatted error message with status code and status text
Error Handling Examples
import { HttpError } from 'http-plz';
try {
const response = await api.get({ path: '/users/999' });
} catch (error) {
if (error instanceof HttpError) {
// Access specific error information
console.error(`Status: ${error.response.status}`);
console.error(`Status Text: ${error.response.statusText}`);
console.error(`Error Body:`, error.body);
console.error(`Request URL: ${error.response.url}`);
// Handle specific status codes
switch (error.response.status) {
case 404:
console.error('Resource not found');
break;
case 401:
console.error('Unauthorized - check your credentials');
break;
case 500:
console.error('Server error - try again later');
break;
default:
console.error('Request failed:', error.message);
}
}
}The library doesn't perform any automatic error recovery or retries - it simply formats errors consistently and lets you handle them according to your application's needs.
Middleware System
The middleware system in this library is designed to be simple, powerful, and predictable. It's based on "stacks" (execution chains) that clearly separate the request logic from the response logic, much like Axios interceptors, but with a more explicit approach.
The main reason for this architecture is simplicity and clarity. Instead of a single, overloaded interceptor, you have two distinct chains, each with a single, clear responsibility.
Execution Order: Symmetry is Key
For complete control, each stack executes in a specific order, creating a symmetrical "wrapping" effect:
- Request Stack: Executes in FIFO (First-In, First-Out) order. The first middleware you define is the first one to run.
- Response Stack: Executes in FILO (First-In, Last-Out) order. The last middleware you define is the first one to process the response.
This FILO order for the response is crucial. It means that the first middleware to "wrap" the request is the last one to "unwrap" the response. This creates perfect symmetry, ideal for tasks like timing or logging, ensuring that the start and end of a single feature execute in the correct layers.
Practical Example: The Execution Flow
Imagine you register two middlewares for each stack to see this behavior in action.
Middleware Definitions:
const requestMiddleware1 = async (options) => {
console.log("1. Request Middleware 1 (Outer Layer)");
return options;
};
const requestMiddleware2 = async (options) => {
console.log("2. Request Middleware 2 (Inner Layer)");
return options;
};
const responseMiddleware1 = async (response) => {
console.log("5. Response Middleware 1 (Outer Layer)");
return response;
};
const responseMiddleware2 = async (response) => {
console.log("4. Response Middleware 2 (Inner Layer)");
return response;
};
// Client registration
const client = createClient({
requestMiddlewares: [requestMiddleware1, requestMiddleware2],
responseMiddlewares: [responseMiddleware1, responseMiddleware2],
});
client.get({ path: '/test' });Console Output:
1. Request Middleware 1 (Outer Layer)
2. Request Middleware 2 (Inner Layer)
3. [HTTP Request is made]
4. Response Middleware 2 (Inner Layer)
5. Response Middleware 1 (Outer Layer)As you can see, the flow is like an onion. Request Middleware 1 and Response Middleware 1 act as the outermost layer, while Request Middleware 2 and Response Middleware 2 are the inner layer that directly wraps the fetch call. This FILO order in the response is what guarantees this symmetry, allowing you to manage a "layer's" state or logic predictably.
Request Body and Headers
The library automatically handles request bodies and headers based on the input type, determining the appropriate content type and processing method for each body format.
Body Processing
The library uses the processBody utility to automatically detect and handle different body types:
JSON Objects
await api.post({
path: '/users',
body: { name: 'John', email: '[email protected]' }
// Automatically stringified with Content-Type: application/json
});FormData
const formData = new FormData();
formData.append('name', 'John');
formData.append('file', fileInput);
await api.post({
path: '/upload',
body: formData
// Content-Type: multipart/form-data (set automatically by browser)
});URLSearchParams
const params = new URLSearchParams();
params.append('name', 'John');
params.append('email', '[email protected]');
await api.post({
path: '/form-submit',
body: params
// Content-Type: application/x-www-form-urlencoded
});String Data
await api.post({
path: '/text',
body: 'Plain text content'
// Sent as-is (no automatic Content-Type)
});Binary Data (Blob/ArrayBuffer)
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
await api.post({
path: '/binary',
body: blob
// Sent as-is (Content-Type from blob if available)
});Header Management
Headers are merged in the following priority order (highest to lowest):
- Per-request headers (in
opts.headers) - Automatic Content-Type (based on body type)
- Base configuration headers (from
config.options.headers)
Example: Header Precedence
const api = httpPlz({
baseURL: 'https://api.example.com',
options: {
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json', // Will be overridden
},
},
});
await api.post({
path: '/upload',
body: formData, // Sets Content-Type: multipart/form-data
opts: {
headers: {
'X-Custom-Header': 'custom-value', // Highest priority
},
},
});
// Final headers:
// - Authorization: Bearer token (from base config)
// - Content-Type: multipart/form-data (automatic, overrides base)
// - X-Custom-Header: custom-value (per-request)Disabling Automatic Content-Type
await api.post({
path: '/custom',
body: { data: 'value' },
opts: {
headers: {
'Content-Type': 'application/custom+json', // Override automatic detection
},
},
});The library handles content processing transparently - you simply provide the body in the format that makes sense for your use case, and the appropriate headers and encoding are applied automatically.
Response Object
The library returns an enhanced Response object with an additional data property:
interface httpResponse<T = unknown> extends Response {
data?: T;
}Development
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Build the library
pnpm build
# Lint code
pnpm lint:check
# Format code
pnpm format:checkContributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
All commits must follow the Conventional Commits specification. This allows for automated versioning and changelog generation.
The release process is automated. A new release is created by pushing a commit to main that contains the keyword [release] in its message body.
A typical contribution workflow is:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
If you find this library helpful, please consider giving it a ⭐ on GitHub!
For questions, issues, or feature requests, please open an issue on the GitHub repository.
