437 lines
13 KiB
Markdown
437 lines
13 KiB
Markdown
# @push.rocks/smartrequest
|
|
A modern HTTP/HTTPS request library for Node.js with a fetch-like API, supporting form data, file uploads, JSON, binary data, streams, and unix sockets.
|
|
|
|
## Install
|
|
```bash
|
|
# Using npm
|
|
npm install @push.rocks/smartrequest --save
|
|
|
|
# Using pnpm
|
|
pnpm add @push.rocks/smartrequest
|
|
|
|
# Using yarn
|
|
yarn add @push.rocks/smartrequest
|
|
```
|
|
|
|
## Key Features
|
|
|
|
- 🚀 **Modern Fetch-like API** - Familiar response methods (`.json()`, `.text()`, `.arrayBuffer()`, `.stream()`)
|
|
- 🌐 **Unix Socket Support** - Connect to local services like Docker
|
|
- 📦 **Form Data & File Uploads** - Built-in support for multipart/form-data
|
|
- 🔁 **Pagination Support** - Multiple strategies (offset, cursor, Link headers)
|
|
- ⚡ **Keep-Alive Connections** - Efficient connection pooling
|
|
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
|
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
|
- 🔌 **Streaming Support** - Handle large files and real-time data
|
|
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
|
|
- 🔄 **Legacy API Available** - For backward compatibility
|
|
|
|
## Usage
|
|
|
|
`@push.rocks/smartrequest` provides a clean, type-safe API inspired by the native fetch API but with additional features needed for server-side applications.
|
|
|
|
### Basic Usage
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
// Simple GET request
|
|
async function fetchUserData(userId: number) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(`https://jsonplaceholder.typicode.com/users/${userId}`)
|
|
.get();
|
|
|
|
// Use the fetch-like response API
|
|
const userData = await response.json();
|
|
console.log(userData); // The parsed JSON response
|
|
}
|
|
|
|
// POST request with JSON body
|
|
async function createPost(title: string, body: string, userId: number) {
|
|
const response = await SmartRequestClient.create()
|
|
.url('https://jsonplaceholder.typicode.com/posts')
|
|
.json({ title, body, userId })
|
|
.post();
|
|
|
|
const createdPost = await response.json();
|
|
console.log(createdPost); // The created post
|
|
}
|
|
```
|
|
|
|
### Setting Headers and Query Parameters
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
async function searchRepositories(query: string, perPage: number = 10) {
|
|
const response = await SmartRequestClient.create()
|
|
.url('https://api.github.com/search/repositories')
|
|
.header('Accept', 'application/vnd.github.v3+json')
|
|
.query({
|
|
q: query,
|
|
per_page: perPage.toString()
|
|
})
|
|
.get();
|
|
|
|
const data = await response.json();
|
|
return data.items;
|
|
}
|
|
```
|
|
|
|
### Handling Timeouts and Retries
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
async function fetchWithRetry(url: string) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.timeout(5000) // 5 seconds timeout
|
|
.retry(3) // Retry up to 3 times on failure
|
|
.get();
|
|
|
|
return await response.json();
|
|
}
|
|
```
|
|
|
|
### Working with Different Response Types
|
|
|
|
The API provides a fetch-like interface for handling different response types:
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
// JSON response (default)
|
|
async function fetchJson(url: string) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.get();
|
|
|
|
return await response.json(); // Parses JSON automatically
|
|
}
|
|
|
|
// Text response
|
|
async function fetchText(url: string) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.get();
|
|
|
|
return await response.text(); // Returns response as string
|
|
}
|
|
|
|
// Binary data
|
|
async function downloadImage(url: string) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.accept('binary') // Optional: hints to server we want binary
|
|
.get();
|
|
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer); // Convert ArrayBuffer to Buffer if needed
|
|
}
|
|
|
|
// Streaming response
|
|
async function streamLargeFile(url: string) {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.get();
|
|
|
|
// Get the underlying Node.js stream
|
|
const stream = response.stream();
|
|
|
|
stream.on('data', (chunk) => {
|
|
console.log(`Received ${chunk.length} bytes of data`);
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
stream.on('end', resolve);
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Response Object Methods
|
|
|
|
The `SmartResponse` object provides these methods:
|
|
|
|
- `json<T>(): Promise<T>` - Parse response as JSON
|
|
- `text(): Promise<string>` - Get response as text
|
|
- `arrayBuffer(): Promise<ArrayBuffer>` - Get response as ArrayBuffer
|
|
- `stream(): NodeJS.ReadableStream` - Get the underlying Node.js stream
|
|
- `raw(): http.IncomingMessage` - Get the raw http.IncomingMessage
|
|
|
|
Each body method can only be called once per response, similar to the fetch API.
|
|
|
|
## Advanced Features
|
|
|
|
### Form Data with File Uploads
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
import * as fs from 'fs';
|
|
|
|
async function uploadMultipleFiles(files: Array<{name: string, path: string}>) {
|
|
const formFields = files.map(file => ({
|
|
name: 'files',
|
|
value: fs.readFileSync(file.path),
|
|
filename: file.name,
|
|
contentType: 'application/octet-stream'
|
|
}));
|
|
|
|
const response = await SmartRequestClient.create()
|
|
.url('https://api.example.com/upload')
|
|
.formData(formFields)
|
|
.post();
|
|
|
|
return await response.json();
|
|
}
|
|
```
|
|
|
|
### Unix Socket Support
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
// Connect to a service via Unix socket
|
|
async function queryViaUnixSocket() {
|
|
const response = await SmartRequestClient.create()
|
|
.url('http://unix:/var/run/docker.sock:/v1.24/containers/json')
|
|
.get();
|
|
|
|
return await response.json();
|
|
}
|
|
```
|
|
|
|
### Pagination Support
|
|
|
|
The library includes built-in support for various pagination strategies:
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
// Offset-based pagination (page & limit)
|
|
async function fetchAllUsers() {
|
|
const client = SmartRequestClient.create()
|
|
.url('https://api.example.com/users')
|
|
.withOffsetPagination({
|
|
pageParam: 'page',
|
|
limitParam: 'limit',
|
|
startPage: 1,
|
|
pageSize: 20,
|
|
totalPath: 'meta.total'
|
|
});
|
|
|
|
// Get first page with pagination info
|
|
const firstPage = await client.getPaginated();
|
|
console.log(`Found ${firstPage.items.length} users on first page`);
|
|
console.log(`Has more pages: ${firstPage.hasNextPage}`);
|
|
|
|
if (firstPage.hasNextPage) {
|
|
// Get next page
|
|
const secondPage = await firstPage.getNextPage();
|
|
console.log(`Found ${secondPage.items.length} more users`);
|
|
}
|
|
|
|
// Or get all pages at once (use with caution for large datasets)
|
|
const allUsers = await client.getAllPages();
|
|
console.log(`Retrieved ${allUsers.length} users in total`);
|
|
}
|
|
|
|
// Cursor-based pagination
|
|
async function fetchAllPosts() {
|
|
const allPosts = await SmartRequestClient.create()
|
|
.url('https://api.example.com/posts')
|
|
.withCursorPagination({
|
|
cursorParam: 'cursor',
|
|
cursorPath: 'meta.nextCursor',
|
|
hasMorePath: 'meta.hasMore'
|
|
})
|
|
.getAllPages();
|
|
|
|
console.log(`Retrieved ${allPosts.length} posts in total`);
|
|
}
|
|
|
|
// Link header-based pagination (GitHub API style)
|
|
async function fetchAllIssues(repo: string) {
|
|
const paginatedResponse = await SmartRequestClient.create()
|
|
.url(`https://api.github.com/repos/${repo}/issues`)
|
|
.header('Accept', 'application/vnd.github.v3+json')
|
|
.withLinkPagination()
|
|
.getPaginated();
|
|
|
|
return paginatedResponse.getAllPages();
|
|
}
|
|
```
|
|
|
|
### Keep-Alive Connections
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
// Enable keep-alive for better performance with multiple requests
|
|
async function performMultipleRequests() {
|
|
const client = SmartRequestClient.create()
|
|
.header('Connection', 'keep-alive');
|
|
|
|
// Requests will reuse the same connection
|
|
const results = await Promise.all([
|
|
client.url('https://api.example.com/endpoint1').get(),
|
|
client.url('https://api.example.com/endpoint2').get(),
|
|
client.url('https://api.example.com/endpoint3').get()
|
|
]);
|
|
|
|
return Promise.all(results.map(r => r.json()));
|
|
}
|
|
```
|
|
|
|
## Complete Example: Building a REST API Client
|
|
|
|
Here's a complete example of building a typed API client:
|
|
|
|
```typescript
|
|
import { SmartRequestClient, type SmartResponse } from '@push.rocks/smartrequest';
|
|
|
|
interface User {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
}
|
|
|
|
interface Post {
|
|
id: number;
|
|
title: string;
|
|
body: string;
|
|
userId: number;
|
|
}
|
|
|
|
class BlogApiClient {
|
|
private baseUrl = 'https://jsonplaceholder.typicode.com';
|
|
|
|
private async request(path: string) {
|
|
return SmartRequestClient.create()
|
|
.url(`${this.baseUrl}${path}`)
|
|
.header('Accept', 'application/json');
|
|
}
|
|
|
|
async getUser(id: number): Promise<User> {
|
|
const response = await this.request(`/users/${id}`).get();
|
|
return response.json<User>();
|
|
}
|
|
|
|
async createPost(post: Omit<Post, 'id'>): Promise<Post> {
|
|
const response = await this.request('/posts')
|
|
.json(post)
|
|
.post();
|
|
return response.json<Post>();
|
|
}
|
|
|
|
async deletePost(id: number): Promise<void> {
|
|
const response = await this.request(`/posts/${id}`).delete();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to delete post: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
async getAllPosts(userId?: number): Promise<Post[]> {
|
|
const client = this.request('/posts');
|
|
|
|
if (userId) {
|
|
client.query({ userId: userId.toString() });
|
|
}
|
|
|
|
const response = await client.get();
|
|
return response.json<Post[]>();
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const api = new BlogApiClient();
|
|
const user = await api.getUser(1);
|
|
const posts = await api.getAllPosts(user.id);
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```typescript
|
|
import { SmartRequestClient } from '@push.rocks/smartrequest';
|
|
|
|
async function fetchWithErrorHandling(url: string) {
|
|
try {
|
|
const response = await SmartRequestClient.create()
|
|
.url(url)
|
|
.timeout(5000)
|
|
.retry(2)
|
|
.get();
|
|
|
|
// Check if request was successful
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
// Handle different content types
|
|
const contentType = response.headers['content-type'];
|
|
|
|
if (contentType?.includes('application/json')) {
|
|
return await response.json();
|
|
} else if (contentType?.includes('text/')) {
|
|
return await response.text();
|
|
} else {
|
|
return await response.arrayBuffer();
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ECONNREFUSED') {
|
|
console.error('Connection refused - is the server running?');
|
|
} else if (error.code === 'ETIMEDOUT') {
|
|
console.error('Request timed out');
|
|
} else {
|
|
console.error('Request failed:', error.message);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Legacy API
|
|
|
|
For backward compatibility, the original function-based API is still available via a separate import:
|
|
|
|
```typescript
|
|
import { getJson, postJson, request } from '@push.rocks/smartrequest/legacy';
|
|
|
|
// Simple GET request
|
|
const response = await getJson('https://api.example.com/data');
|
|
console.log(response.body);
|
|
|
|
// POST request
|
|
const result = await postJson('https://api.example.com/users', {
|
|
requestBody: { name: 'John', email: 'john@example.com' }
|
|
});
|
|
```
|
|
|
|
For migration from the legacy API to the modern API, here's a quick reference:
|
|
|
|
| Legacy API | Modern API |
|
|
|------------|------------|
|
|
| `getJson(url)` | `SmartRequestClient.create().url(url).get()` |
|
|
| `postJson(url, { requestBody: data })` | `SmartRequestClient.create().url(url).json(data).post()` |
|
|
| `request(url, options)` | `SmartRequestClient.create().url(url).[...configure].get()` |
|
|
|
|
## License and Legal Information
|
|
|
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
|
|
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
|
|
### Trademarks
|
|
|
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
|
|
|
### Company Information
|
|
|
|
Task Venture Capital GmbH
|
|
Registered at District court Bremen HRB 35230 HB, Germany
|
|
|
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
|
|
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. |