@digitalbooting/request-api
Version:
Light Weight Request Api for Http requests support GraphQL and Rest
1,049 lines (780 loc) • 21 kB
Markdown
# @digitalbooting/request-api
[](https://www.npmjs.com/package/@digitalbooting/request-api)
[](https://opensource.org/licenses/ISC)
> Lightweight HTTP client for JavaScript with built-in support for REST, GraphQL, encryption, middlewares, and authentication.
## ✨ Features
- 🚀 **Lightweight** - Minimal dependencies, maximum performance
- 🔄 **REST & GraphQL** - Full support for both paradigms
- 🔐 **Built-in Encryption** - AES-256 encryption for sensitive data
- 🎯 **Middleware System** - Chain operations before requests
- 🔑 **Authentication** - Bearer and Basic auth helpers
- 📦 **FormData Support** - File uploads made easy
- ⚡ **Promise-based** - Modern async/await syntax
- 🎨 **Response Pattern** - Consistent `[payload, error]` destructuring
- 🔄 **Auto Header Reset** - Preserves default headers across requests
---
## 📦 Installation
```bash
# npm
npm install @digitalbooting/request-api
# yarn
yarn add @digitalbooting/request-api
# pnpm
pnpm add @digitalbooting/request-api
```
---
## 🚀 Quick Start
### Basic Usage
```javascript
import { createClient } from '@digitalbooting/request-api';
// Create client instance
const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
});
// Make a GET request
const [payload, error] = await api.get('/users');
if (error) {
console.error('Request failed:', error);
} else {
console.log('Users:', payload.data);
}
```
### Using Class Constructor
```javascript
import ApiClient from '@digitalbooting/request-api';
const api = new ApiClient('https://api.example.com', {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
});
```
---
## 📚 API Reference
### Constructor
```javascript
new ApiClient(baseURL, headers?, options?)
```
**Parameters:**
- `baseURL` (string): Base URL for all requests
- `headers` (object, optional): Default headers for all requests
- `options` (object, optional): Additional fetch options
**Example:**
```javascript
const api = new ApiClient('https://api.example.com', {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
}, {
mode: 'cors',
credentials: 'include'
});
```
---
### HTTP Methods
All methods return a Promise that resolves to `[payload, error]` tuple.
#### GET Request
```javascript
await api.get(path)
```
**Example:**
```javascript
const [payload, error] = await api.get('/users');
if (error) {
console.error('Error:', error.data);
} else {
console.log('Users:', payload.data);
console.log('Status:', payload.status);
console.log('Success:', payload.success);
}
```
#### POST Request
```javascript
await api.post(path, body, isFormData?)
```
**Example with JSON:**
```javascript
const [payload, error] = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
```
**Example with FormData:**
```javascript
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('description', 'Profile picture');
const [payload, error] = await api.post('/upload', formData, true);
```
#### PUT Request
```javascript
await api.put(path, body, isFormData?)
```
**Example:**
```javascript
const [payload, error] = await api.put('/users/123', {
name: 'Jane Doe'
});
```
#### DELETE Request
```javascript
await api.delete(path)
```
**Example:**
```javascript
const [payload, error] = await api.delete('/users/123');
```
#### PURE POST (No response parsing)
For endpoints that don't return JSON:
```javascript
await api.pure_post(path, body, isFormData?)
```
**Example:**
```javascript
const [payload, error] = await api.pure_post('/webhook', {
event: 'user.created'
});
// payload.success indicates HTTP status
// payload.status contains the HTTP status code
```
---
### GraphQL
```javascript
await api.graphql(query, variables?, method?)
```
**Parameters:**
- `query` (string): GraphQL query string
- `variables` (object, optional): Query variables
- `method` (string, optional): HTTP method (default: 'POST')
**Example:**
```javascript
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
title
content
}
}
}
`;
const [payload, error] = await api.graphql(query, { id: '123' });
if (!error) {
console.log('User:', payload.data.user);
}
```
**Mutation Example:**
```javascript
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const [payload, error] = await api.graphql(mutation, {
input: {
name: 'John Doe',
email: 'john@example.com'
}
});
```
**Custom GraphQL Endpoint:**
```javascript
const api = new ApiClient('https://api.example.com', {}, {
disableDefaultGraphqlEndpoint: true
});
// Requests will go to baseURL directly
// instead of baseURL/graphql
```
---
### Headers Management
#### Set Headers
```javascript
api.setHeaders(newHeaders)
```
**Example:**
```javascript
api.setHeaders({
'X-API-Key': 'your-api-key',
'X-Custom-Header': 'value'
});
```
#### Reset to Default Headers
```javascript
api.setDefaultHeaders()
```
Headers are automatically reset after each request to the values passed in the constructor.
**Example:**
```javascript
// Initial headers
const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
});
// Add temporary header
api.setHeaders({ 'X-Temp-Header': 'temp' });
// Make request
await api.get('/data');
// X-Temp-Header is automatically removed after request
// Content-Type is preserved for next request
```
---
### Authentication
#### Bearer Token
```javascript
api.setAuth('bearer', token)
```
**Example:**
```javascript
const token = 'eyJhbGciOiJIUzI1NiIs...';
api.setAuth('bearer', token);
// All subsequent requests will include:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
#### Basic Auth
```javascript
api.setAuth('basic', 'username:password')
```
**Example:**
```javascript
api.setAuth('basic', 'admin:secretpass');
// All subsequent requests will include:
// Authorization: Basic YWRtaW46c2VjcmV0cGFzcw==
```
#### JWT Token Check
```javascript
await api.checkJWToken(token)
```
**Example:**
```javascript
const [isValid, error] = await api.checkJWToken(currentToken);
if (!isValid) {
console.log('Token expired:', error);
// Handle re-authentication
}
```
---
### Middleware System
Middlewares are executed before each request. If a middleware returns `false`, the request is blocked.
#### Register Middleware
```javascript
api.registerMiddleware(asyncFunction)
```
**Example: Logging Middleware**
```javascript
api.registerMiddleware(async () => {
console.log('Request initiated at:', new Date().toISOString());
return true; // Continue with request
});
```
**Example: Authentication Check**
```javascript
api.registerMiddleware(async () => {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
return false; // Block request
}
api.setAuth('bearer', token);
return true; // Continue with request
});
```
**Example: Rate Limiting**
```javascript
let lastRequestTime = 0;
const MIN_INTERVAL = 1000; // 1 second
api.registerMiddleware(async () => {
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < MIN_INTERVAL) {
console.warn('Rate limit: Too many requests');
return false;
}
lastRequestTime = now;
return true;
});
```
**Example: Multiple Middlewares**
```javascript
// Logger
api.registerMiddleware(async () => {
console.log('[API] Request starting...');
return true;
});
// Auth check
api.registerMiddleware(async () => {
const token = getToken();
if (!token) return false;
api.setAuth('bearer', token);
return true;
});
// Custom header
api.registerMiddleware(async () => {
api.setHeaders({
'X-Request-ID': generateRequestId()
});
return true;
});
```
---
### Encryption (AES-256)
Built-in AES-256 encryption for sensitive data transmission.
#### Enable Encryption
```javascript
api.enableEncryption(key?, iv?)
```
**Parameters:**
- `key` (string, optional): 32-byte hex key (64 characters)
- `iv` (string, optional): 16-byte hex IV (32 characters)
**Example with Auto-generated Keys:**
```javascript
api.enableEncryption();
// Keys are auto-generated and accessible via:
console.log('Key:', api.key);
console.log('IV:', api.iv);
```
**Example with Custom Keys:**
```javascript
const key = 'a'.repeat(64); // 32 bytes in hex
const iv = 'b'.repeat(32); // 16 bytes in hex
api.enableEncryption(key, iv);
```
#### Set Encryption Key
```javascript
api.setKey(hexKey)
```
**Example:**
```javascript
const key = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
api.setKey(key); // Must be 64 hex characters (32 bytes)
```
#### Set Encryption IV
```javascript
api.setIV(hexIV)
```
**Example:**
```javascript
const iv = '0123456789abcdef0123456789abcdef';
api.setIV(iv); // Must be 32 hex characters (16 bytes)
```
#### Manual Encryption/Decryption
```javascript
const encrypted = api.encrypt(plainText);
const decrypted = api.decrypt(encryptedText);
```
**Example:**
```javascript
api.enableEncryption();
const sensitive = 'Credit card: 1234-5678-9012-3456';
const encrypted = api.encrypt(sensitive);
console.log('Encrypted:', encrypted);
const decrypted = api.decrypt(encrypted);
console.log('Decrypted:', decrypted); // Original text
```
---
### Utility Methods
#### Get Client IP
```javascript
await api.requestIp()
```
**Example:**
```javascript
const ipInfo = await api.requestIp();
console.log('Client IP:', ipInfo.ip);
```
#### Getters
```javascript
api.base_url // Get base URL
api.headers_enabled // Get current headers
api.options_enabled // Get fetch options
api.middlewares_enabled // Get registered middlewares
api.encryption_enabled // Check if encryption is enabled
api.key // Get encryption key
api.iv // Get encryption IV
```
**Example:**
```javascript
console.log('Base URL:', api.base_url);
console.log('Headers:', api.headers_enabled);
console.log('Middlewares:', api.middlewares_enabled.length);
console.log('Encrypted:', api.encryption_enabled);
```
---
## 🎯 Response Format
All HTTP methods return a tuple `[payload, error]`:
### Success Response
```javascript
const [payload, error] = await api.get('/users');
if (!error) {
payload.data // Response data
payload.success // true
payload.status // HTTP status code (200, 201, etc.)
}
```
### Error Response
```javascript
const [payload, error] = await api.post('/users', invalidData);
if (error) {
error.data // Error data from server
error.response // Raw response object
error.success // false
error.status // HTTP status code (400, 500, etc.)
}
```
### Usage Pattern
```javascript
// Destructure and check
const [payload, error] = await api.get('/users');
if (error) {
console.error('Request failed:', error.data);
return;
}
console.log('Success:', payload.data);
```
---
## 💡 Advanced Examples
### Complete Authentication Flow
```javascript
import { createClient } from '@digitalbooting/request-api';
const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
});
// Login
async function login(email, password) {
const [payload, error] = await api.post('/auth/signin', {
email,
password
});
if (error) {
throw new Error(error.data?.message || 'Login failed');
}
// Store token
localStorage.setItem('auth_token', payload.data.token);
// Set for future requests
api.setAuth('bearer', payload.data.token);
return payload.data.user;
}
// Logout
async function logout() {
const [payload, error] = await api.post('/auth/signout');
localStorage.removeItem('auth_token');
api.setDefaultHeaders(); // Clear auth header
return !error;
}
// Auto-attach token middleware
api.registerMiddleware(async () => {
const token = localStorage.getItem('auth_token');
if (token) {
api.setAuth('bearer', token);
}
return true;
});
```
### File Upload with Progress
```javascript
async function uploadFile(file, onProgress) {
const formData = new FormData();
formData.append('file', file);
formData.append('timestamp', Date.now());
// Add progress tracking middleware
api.registerMiddleware(async () => {
if (onProgress) {
onProgress({ status: 'uploading', progress: 0 });
}
return true;
});
const [payload, error] = await api.post('/upload', formData, true);
if (error) {
if (onProgress) {
onProgress({ status: 'error', error: error.data });
}
return null;
}
if (onProgress) {
onProgress({ status: 'complete', data: payload.data });
}
return payload.data;
}
// Usage
uploadFile(myFile, (progress) => {
console.log('Upload:', progress);
});
```
### API Service Class
```javascript
import { createClient } from '@digitalbooting/request-api';
class UserService {
constructor(baseURL) {
this.api = createClient(baseURL, {
'Content-Type': 'application/json'
});
this.setupMiddlewares();
}
setupMiddlewares() {
// Auth middleware
this.api.registerMiddleware(async () => {
const token = this.getToken();
if (token) {
this.api.setAuth('bearer', token);
}
return true;
});
// Logging middleware
this.api.registerMiddleware(async () => {
console.log('[UserService] Request initiated');
return true;
});
}
getToken() {
return localStorage.getItem('auth_token');
}
async getAll() {
const [payload, error] = await this.api.get('/users');
if (error) throw new Error(error.data?.message);
return payload.data;
}
async getById(id) {
const [payload, error] = await this.api.get(`/users/${id}`);
if (error) throw new Error(error.data?.message);
return payload.data;
}
async create(userData) {
const [payload, error] = await this.api.post('/users', userData);
if (error) throw new Error(error.data?.message);
return payload.data;
}
async update(id, userData) {
const [payload, error] = await this.api.put(`/users/${id}`, userData);
if (error) throw new Error(error.data?.message);
return payload.data;
}
async delete(id) {
const [payload, error] = await this.api.delete(`/users/${id}`);
if (error) throw new Error(error.data?.message);
return payload.success;
}
}
// Usage
const userService = new UserService('https://api.example.com');
const users = await userService.getAll();
const user = await userService.create({ name: 'John', email: 'john@example.com' });
```
### GraphQL with Encryption
```javascript
const api = createClient('https://api.example.com');
// Enable encryption
api.enableEncryption();
// Send encrypted GraphQL query
const query = `
query GetSensitiveData {
user {
ssn
creditCard
}
}
`;
const [payload, error] = await api.graphql(query);
// The query is automatically encrypted before sending
// Response is automatically decrypted (if server supports it)
```
### Error Handling Patterns
```javascript
// Pattern 1: Inline checking
const [payload, error] = await api.get('/users');
if (error) {
console.error('Error:', error.data);
return;
}
console.log('Data:', payload.data);
// Pattern 2: Try-catch wrapper
async function fetchUsers() {
const [payload, error] = await api.get('/users');
if (error) {
throw new Error(error.data?.message || 'Failed to fetch users');
}
return payload.data;
}
try {
const users = await fetchUsers();
console.log('Users:', users);
} catch (err) {
console.error('Error:', err.message);
}
// Pattern 3: Custom error handler
function handleApiError(error) {
if (error.status === 401) {
// Unauthorized - redirect to login
window.location.href = '/login';
} else if (error.status === 403) {
// Forbidden
alert('You do not have permission to perform this action');
} else if (error.status === 500) {
// Server error
console.error('Server error:', error.data);
} else {
// Generic error
console.error('Request failed:', error.data);
}
}
const [payload, error] = await api.get('/users');
if (error) {
handleApiError(error);
return;
}
console.log('Data:', payload.data);
```
---
## 🔧 Configuration
### Fetch Options
You can pass any valid fetch options:
```javascript
const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
}, {
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
redirect: 'follow',
referrerPolicy: 'no-referrer'
});
```
### Custom Options per Request
```javascript
api.setOptions({
signal: abortController.signal,
priority: 'high'
});
const [payload, error] = await api.get('/users');
```
---
## 📝 Best Practices
### 1. Use Destructuring Pattern
```javascript
// ✅ Good
const [payload, error] = await api.get('/users');
if (error) {
// Handle error
}
// ❌ Bad
const response = await api.get('/users');
if (response[1]) {
// Handle error
}
```
### 2. Check Errors First
```javascript
// ✅ Good
const [payload, error] = await api.post('/users', data);
if (error) {
console.error('Failed:', error.data);
return;
}
console.log('Success:', payload.data);
// ❌ Bad
const [payload, error] = await api.post('/users', data);
console.log('Success:', payload.data); // May crash if error
```
### 3. Reuse Client Instances
```javascript
// ✅ Good - Single instance
// api.js
export const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
});
// users.js
import { api } from './api';
const [payload, error] = await api.get('/users');
// ❌ Bad - Multiple instances
const api1 = createClient('https://api.example.com');
const api2 = createClient('https://api.example.com');
```
### 4. Use Middlewares for Cross-cutting Concerns
```javascript
// ✅ Good - Centralized auth
api.registerMiddleware(async () => {
const token = getToken();
if (token) api.setAuth('bearer', token);
return true;
});
// ❌ Bad - Manual auth each time
api.setAuth('bearer', getToken());
await api.get('/users');
api.setAuth('bearer', getToken());
await api.get('/posts');
```
### 5. Handle FormData Correctly
```javascript
// ✅ Good
const formData = new FormData();
formData.append('file', file);
const [payload, error] = await api.post('/upload', formData, true);
// ^^^^ isFormData
// ❌ Bad - Will stringify FormData
const [payload, error] = await api.post('/upload', formData);
```
---
## 🐛 Troubleshooting
### Headers Not Persisting
**Problem:** Headers are lost after first request.
**Solution:** This was fixed in v1.0.7. Make sure you're using the latest version:
```bash
npm install @digitalbooting/request-api@latest
```
### CORS Errors
**Problem:** Request blocked by CORS policy.
**Solution:** Configure fetch options:
```javascript
const api = createClient('https://api.example.com', {
'Content-Type': 'application/json'
}, {
mode: 'cors',
credentials: 'include'
});
```
### FormData Not Sending
**Problem:** FormData sent as JSON.
**Solution:** Pass `true` as third parameter:
```javascript
const [payload, error] = await api.post('/upload', formData, true);
// ^^^^
```
### Middleware Blocking Requests
**Problem:** Request never executes.
**Solution:** Ensure middleware returns `true`:
```javascript
api.registerMiddleware(async () => {
// Your logic here
return true; // ← Must return true to continue
});
```
---
## 📊 Changelog
### v1.0.7 (Current)
- 🐛 **Fix:** Default headers now properly preserved across requests
- 🔧 **Fix:** `#initialHeaders` correctly initialized in constructor
- ✨ **Improvement:** Headers automatically reset to constructor values after each request
### v1.0.6
- 🐛 **Bug:** Headers reset to empty object after each request (fixed in 1.0.7)
---
## 📄 License
ISC © Leonardo Quintana
---
## 🤝 Contributing
Contributions are welcome! Please follow these guidelines:
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Open a Pull Request
---
## 📮 Support
- **Issues:** [GitLab Issues](https://gitlab.com/digitalbooting/request-api/-/issues)
- **Repository:** [GitLab](https://gitlab.com/digitalbooting/request-api)
- **NPM:** [@digitalbooting/request-api](https://www.npmjs.com/package/@digitalbooting/request-api)
---
## 🌟 Show Your Support
Give a ⭐️ if this project helped you!
---
**Made with ❤️ by Digital Booting**