UNPKG

@digitalbooting/request-api

Version:

Light Weight Request Api for Http requests support GraphQL and Rest

1,049 lines (780 loc) 21 kB
# @digitalbooting/request-api [![npm version](https://img.shields.io/npm/v/@digitalbooting/request-api.svg)](https://www.npmjs.com/package/@digitalbooting/request-api) [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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**