Middleware System
Request/response middleware and custom middleware creation
The middleware system allows you to intercept and modify requests and responses. Middleware can transform data, add authentication, handle errors, and more.
Built-in Middleware
AuthorizationMiddleware
Adds Bearer token authentication to requests.
import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new AuthorizationMiddleware({ token: 'your-api-token' })
]
});
What it does:
- Adds
Authorization: Bearer {token}header to all requests - Enables authenticated API access
CriteriaMiddleware
Converts Criteria objects to query parameters.
import { Client, CriteriaMiddleware, Criteria } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new CriteriaMiddleware()
]
});
// Criteria is automatically converted to query params
const criteria = new Criteria()
.where('status', 'published')
.limit(10);
const response = await client.entries.get('posts', { criteria });
What it does:
- Transforms
Criteriaobjects into URL query parameters - Enables fluent query building with Laravel-style syntax
QueryMiddleware
Adds query parameters to requests.
import { Client, QueryMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new QueryMiddleware()
]
});
// Add query params via options
const response = await client.get('/posts', {
query: {
status: 'published',
limit: 10
}
});
// GET /posts?status=published&limit=10
What it does:
- Converts
queryoption to URL query string - Handles URL encoding automatically
RequestDataMiddleware
Transforms request data based on content type.
import { Client, RequestDataMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new RequestDataMiddleware()
]
});
// JSON (default)
await client.post('/posts', {
data: { title: 'Hello', content: 'World' }
});
// Content-Type: application/json
// Body: {"title":"Hello","content":"World"}
// Form data
await client.post('/upload', {
data: { file: fileBlob },
headers: { 'Content-Type': 'multipart/form-data' }
});
// Content-Type: multipart/form-data
// Body: FormData object
What it does:
- JSON: Stringifies data and sets
Content-Type: application/json - Form Data: Converts to FormData and sets
Content-Type: multipart/form-data - URL Encoded: Converts to URLSearchParams and sets
Content-Type: application/x-www-form-urlencoded
ResponseDataMiddleware
Parses response data based on content type.
import { Client, ResponseDataMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ResponseDataMiddleware()
]
});
const response = await client.get('/posts');
// Automatically parses JSON response
console.log(response.data); // Parsed object
What it does:
- Parses JSON responses automatically
- Handles text responses
- Returns raw response for other content types
ETagMiddleware
Implements HTTP caching with ETags.
import { Client, ETagMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ETagMiddleware()
]
});
// First request - stores ETag
const response1 = await client.get('/posts/1');
// Response includes: ETag: "abc123"
// Second request - sends If-None-Match
const response2 = await client.get('/posts/1');
// Request includes: If-None-Match: "abc123"
// If not modified: 304 status, returns cached data
What it does:
- Stores ETags from responses
- Adds
If-None-Matchheader on subsequent requests - Returns cached data for 304 (Not Modified) responses
- Reduces bandwidth and improves performance
Creating Custom Middleware
Basic Middleware
import { Middleware } from '@laravel-streams/api-client';
class LoggingMiddleware extends Middleware {
async handle(request, next) {
console.log('Request:', request.url);
const response = await next(request);
console.log('Response:', response.status);
return response;
}
}
// Use it
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new LoggingMiddleware()
]
});
Request Transformation
class ApiVersionMiddleware extends Middleware {
constructor(version = 'v1') {
super();
this.version = version;
}
async handle(request, next) {
// Add version to URL
request.url = `/api/${this.version}${request.url}`;
// Add version header
request.headers.set('X-API-Version', this.version);
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ApiVersionMiddleware('v2')
]
});
await client.get('/posts');
// GET https://api.example.com/api/v2/posts
// Headers: X-API-Version: v2
Response Transformation
class DataWrapperMiddleware extends Middleware {
async handle(request, next) {
const response = await next(request);
// Unwrap nested data structure
if (response.data && response.data.data) {
response.data = response.data.data;
}
return response;
}
}
Error Handling
class RetryMiddleware extends Middleware {
constructor(maxRetries = 3) {
super();
this.maxRetries = maxRetries;
}
async handle(request, next) {
let lastError;
for (let i = 0; i < this.maxRetries; i++) {
try {
return await next(request);
} catch (error) {
lastError = error;
// Only retry on network errors or 5xx status
if (error.status < 500) {
throw error;
}
// Wait before retry
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
throw lastError;
}
}
Request/Response Logging
class DetailedLoggingMiddleware extends Middleware {
async handle(request, next) {
const startTime = Date.now();
console.log('→ Request:', {
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
body: request.body
});
try {
const response = await next(request);
const duration = Date.now() - startTime;
console.log('← Response:', {
status: response.status,
statusText: response.statusText,
duration: `${duration}ms`,
headers: Object.fromEntries(response.headers.entries()),
data: response.data
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error('✗ Error:', {
duration: `${duration}ms`,
message: error.message,
status: error.status
});
throw error;
}
}
}
Real-World Examples
API Key Authentication
class ApiKeyMiddleware extends Middleware {
constructor(apiKey) {
super();
this.apiKey = apiKey;
}
async handle(request, next) {
request.headers.set('X-API-Key', this.apiKey);
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ApiKeyMiddleware('your-api-key-here')
]
});
Request Throttling
class ThrottleMiddleware extends Middleware {
constructor(requestsPerSecond = 10) {
super();
this.delay = 1000 / requestsPerSecond;
this.lastRequest = 0;
}
async handle(request, next) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequest;
if (timeSinceLastRequest < this.delay) {
await new Promise(resolve =>
setTimeout(resolve, this.delay - timeSinceLastRequest)
);
}
this.lastRequest = Date.now();
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ThrottleMiddleware(5) // Max 5 requests per second
]
});
Response Caching
class CacheMiddleware extends Middleware {
constructor(ttl = 60000) { // 60 seconds default
super();
this.cache = new Map();
this.ttl = ttl;
}
getCacheKey(request) {
return `${request.method}:${request.url}`;
}
async handle(request, next) {
// Only cache GET requests
if (request.method !== 'GET') {
return next(request);
}
const cacheKey = this.getCacheKey(request);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('Cache hit:', cacheKey);
return cached.response;
}
const response = await next(request);
this.cache.set(cacheKey, {
response: response,
timestamp: Date.now()
});
return response;
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new CacheMiddleware(30000) // Cache for 30 seconds
]
});
Request/Response Transformation
class TimestampMiddleware extends Middleware {
async handle(request, next) {
// Add timestamp to requests
if (request.body && typeof request.body === 'object') {
request.body.timestamp = Date.now();
}
const response = await next(request);
// Parse date strings in response
if (response.data) {
this.parseDates(response.data);
}
return response;
}
parseDates(obj) {
const dateFields = ['created_at', 'updated_at', 'published_at'];
for (const key in obj) {
if (dateFields.includes(key) && typeof obj[key] === 'string') {
obj[key] = new Date(obj[key]);
} else if (typeof obj[key] === 'object') {
this.parseDates(obj[key]);
}
}
}
}
Middleware Execution Order
Middleware executes in the order it's added:
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new LoggingMiddleware(), // 1. Logs request
new AuthMiddleware(), // 2. Adds auth
new CriteriaMiddleware(), // 3. Converts criteria
new RequestDataMiddleware(), // 4. Transforms request data
// --- Request sent ---
// --- Response received ---
new ResponseDataMiddleware(), // 5. Parses response data
new DataWrapperMiddleware() // 6. Unwraps data
]
});
The execution flow:
- Request goes through middleware from top to bottom
- Response comes back through middleware from bottom to top
- Each middleware can modify request before calling
next() - Each middleware can modify response after calling
next()
Best Practices
Keep Middleware Focused
// Good: Single responsibility
class AuthMiddleware extends Middleware {
async handle(request, next) {
request.headers.set('Authorization', `Bearer ${this.token}`);
return next(request);
}
}
// Bad: Too many responsibilities
class MegaMiddleware extends Middleware {
async handle(request, next) {
// Adds auth, transforms data, logs, caches, retries...
// Too much in one middleware!
}
}
Error Handling
class SafeMiddleware extends Middleware {
async handle(request, next) {
try {
// Your middleware logic
return await next(request);
} catch (error) {
// Handle or re-throw
console.error('Middleware error:', error);
throw error;
}
}
}
Configuration
class ConfigurableMiddleware extends Middleware {
constructor(options = {}) {
super();
this.options = {
enabled: true,
timeout: 5000,
...options
};
}
async handle(request, next) {
if (!this.options.enabled) {
return next(request);
}
// Use this.options...
return next(request);
}
}
Next Steps
- Examples - See middleware in real-world scenarios
- Client Configuration - Learn more about client setup