TypeScript-ready Express middleware for proxying API requests with zero configuration needed. Perfect for API gateways and microservices.
http-proxy-middleware.
npm install express-simple-proxy- Quick Start
- Core Concepts
- Configuration
- Advanced Usage
- Use Cases & Examples
- Lifecycle Hooks
- Error Handling
- API Reference
- Development & Testing
- Contributing
import express from 'express';
import { createProxyController } from 'express-simple-proxy';
const app = express();
const proxy = createProxyController({
baseURL: 'https://api.example.com'
});
// Direct path mapping - no configuration needed
app.get('/users', proxy());
app.post('/users', proxy());
app.get('/users/:id', proxy());const proxy = createProxyController({
baseURL: 'https://api.example.com',
headers: (req) => ({
'Authorization': `Bearer ${req.headers.authorization}`,
'User-Agent': 'MyApp/1.0'
})
});
app.use('/api', proxy()); // Proxy all /api/* routesconst proxy = createProxyController({
baseURL: 'https://api.example.com',
headers: (req) => ({ 'Authorization': req.headers.authorization }),
errorHandler: (error, req, res) => {
res.status(error.status || 500).json({
success: false,
error: error.message,
timestamp: new Date().toISOString()
});
}
});The key differentiator is omitted proxy paths - when you call proxy() without a path parameter, it uses the original request path:
// Traditional proxy libraries require explicit path mapping:
app.get('/users', proxy('/api/users')); // Maps /users → /api/users
app.get('/users/:id', proxy('/api/users/:id')); // Maps /users/123 → /api/users/123
// Express Simple Proxy - zero configuration:
app.get('/users', proxy()); // Maps /users → /users
app.get('/users/:id', proxy()); // Maps /users/123 → /users/123Benefits:
- ✅ Zero Configuration: No path mapping needed
- ✅ Consistent Routing: Frontend and backend paths stay in sync
- ✅ Automatic Parameter Handling: All path parameters are preserved
- ✅ Perfect for Microservices: Direct service-to-service communication
Built from the ground up with TypeScript, not retrofitted:
import { ProxyConfig, ProxyError, RequestWithLocals } from 'express-simple-proxy';
const config: ProxyConfig = {
baseURL: 'https://api.example.com',
headers: (req: RequestWithLocals) => ({
'Authorization': `Bearer ${req.locals?.token}`
}),
errorHandler: (error: ProxyError, req: RequestWithLocals, res: Response) => {
// Full type safety throughout
}
};Optimized specifically for REST API communication:
| Feature | Express Simple Proxy | General HTTP Proxies |
|---|---|---|
| JSON APIs | ✅ Optimized handling | |
| File Uploads | ✅ Built-in multipart/form-data | ❌ Manual setup |
| Error Processing | ✅ Structured error hooks | |
| TypeScript | ✅ Native & complete | |
| Setup Complexity | 🟢 Minimal | 🟡 Configuration heavy |
| Option | Type | Required | Description |
|---|---|---|---|
baseURL |
string |
✅ | Base URL for the target API |
headers |
function |
✅ | Function that returns headers object based on request |
timeout |
number |
❌ | Request timeout in milliseconds (default: 30000) |
responseHeaders |
function |
❌ | Function to transform response headers |
errorHandler |
function |
❌ | Custom error handling function |
errorHandlerHook |
function |
❌ | Error processing hook function |
beforeRequest |
function |
❌ | Hook to mutate the payload or short-circuit with a custom response before the upstream call |
onResponse |
function |
❌ | Callback fired once per request on every terminal path (upstream, short-circuit, error) with traffic stats |
const config: ProxyConfig = {
baseURL: 'https://api.example.com',
timeout: 30000,
headers: (req) => ({
'Authorization': `Bearer ${req.locals.token}`,
'Content-Type': 'application/json',
'X-Request-ID': req.headers['x-request-id']
}),
responseHeaders: (response) => ({
'X-Proxy-Response': 'true',
'X-Response-Time': Date.now().toString()
}),
// Error processing hook - runs before error handler
errorHandlerHook: async (error, req, res) => {
// Log to monitoring service
await logErrorToService(error, req);
// Add context to error
error.context = `${req.method} ${req.path}`;
return error;
},
// Custom error response
errorHandler: (error, req, res) => {
const response = {
success: false,
error: {
message: error.message,
code: error.code,
status: error.status
},
meta: {
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id'],
path: req.path
}
};
res.status(error.status || 500).json(response);
}
};import multer from 'multer';
const upload = multer({ storage: multer.memoryStorage() });
// Single file upload
app.post('/upload', upload.single('file'), proxy());
// Multiple file upload
app.post('/upload-multiple', upload.array('files'), proxy());
// Form data with file
app.post('/profile', upload.single('avatar'), proxy());// Transform response data
app.get('/users', proxy(undefined, (req, res, remoteResponse) => {
res.json({
success: true,
data: remoteResponse.data,
timestamp: new Date().toISOString()
});
}));
// Return raw response
app.get('/raw-data', proxy(undefined, true));// Explicit path mapping for different frontend/backend structures
app.get('/dashboard/users', proxy('/api/admin/users'));
app.get('/public/health', proxy('/internal/health-check'));
// API version mapping
app.get('/v1/users', proxy('/api/v1/users'));
app.get('/latest/users', proxy('/api/v3/users'));const userService = createProxyController({
baseURL: 'https://user-service.internal',
headers: (req) => ({ 'Authorization': req.headers.authorization })
});
const orderService = createProxyController({
baseURL: 'https://order-service.internal',
headers: (req) => ({ 'Authorization': req.headers.authorization })
});
// Clean service routing with omitted paths
app.get('/api/users', userService());
app.post('/api/users', userService());
app.get('/api/users/:id', userService());
app.get('/api/orders', orderService());
app.post('/api/orders', orderService());
app.get('/api/orders/:id', orderService());const tenantProxy = createProxyController({
baseURL: 'https://tenant-api.saas.com',
headers: (req) => ({
'Authorization': req.headers.authorization,
'X-Tenant-ID': req.params.tenantId
})
});
// All tenant routes use direct mapping
app.get('/api/tenants/:tenantId/users', tenantProxy());
app.get('/api/tenants/:tenantId/billing', tenantProxy());
app.get('/api/tenants/:tenantId/analytics', tenantProxy());const devProxy = createProxyController({
baseURL: process.env.API_BASE_URL || 'https://api-dev.company.com',
headers: (req) => ({
'Authorization': req.headers.authorization,
'X-Environment': 'development'
})
});
// Mirror production API structure exactly
app.use('/api', devProxy()); // Catch-all for all API routesconst createServiceProxy = (serviceName: string) => {
return createProxyController({
baseURL: `https://${serviceName}.mesh.internal`,
headers: (req) => ({
'Authorization': req.headers.authorization,
'X-Correlation-ID': req.headers['x-correlation-id'] || generateId(),
'X-Service-Name': serviceName
})
});
};
const userService = createServiceProxy('user-service');
const notificationService = createServiceProxy('notification-service');
// Service mesh routing with consistent paths
app.get('/api/users', userService());
app.get('/api/notifications', notificationService());A comprehensive collection of practical examples for common use cases. See the Complete Cookbook for detailed recipes.
Authentication & Security:
// JWT Token Forwarding
const proxy = createProxyController({
baseURL: 'https://api.example.com',
headers: (req) => ({
'Authorization': req.headers.authorization,
'X-User-ID': req.user?.id,
'X-Request-ID': crypto.randomUUID()
})
});File Uploads:
import multer from 'multer';
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }
});
app.post('/upload', upload.single('file'), proxy('/api/upload'));Load Balancing:
const servers = ['https://api1.com', 'https://api2.com', 'https://api3.com'];
let current = 0;
app.use('/api', (req, res, next) => {
const selectedServer = servers[current++ % servers.length];
const proxy = createProxyController({
baseURL: selectedServer,
headers: (req) => ({ 'Authorization': req.headers.authorization })
});
proxy()(req, res, next);
});Performance Monitoring:
const proxy = createProxyController({
baseURL: 'https://api.example.com',
errorHandlerHook: async (error, req, res) => {
console.log({
method: req.method,
path: req.path,
duration: Date.now() - req.startTime,
status: error.status
});
return error;
}
});📖 View Complete Cookbook - Contains 50+ recipes for:
- Authentication & Security (JWT, API Keys, OAuth2)
- File Handling (Uploads, Validation, Chunking)
- Database & Caching (Sharding, Redis, Cache Control)
- Monitoring & Observability (Tracing, Metrics, Health Checks)
- Load Balancing & Failover (Round Robin, Circuit Breakers)
- Development & Testing (Mocking, A/B Testing, Feature Flags)
- Rate Limiting & Throttling (Basic, Per-user)
- Data Transformation (Schema Validation, GraphQL)
- Content Negotiation (Accept Headers, Compression)
Runs after the payload is assembled but before the upstream call. Return a ShortCircuitResponse to skip the upstream entirely.
const proxy = createProxyController({
baseURL: 'https://api.example.com',
headers: () => ({}),
// Inject headers before forwarding
beforeRequest: (payload, req) => {
payload.headers['X-Request-ID'] = crypto.randomUUID();
payload.headers['X-User-ID'] = req.locals?.userId;
},
});
// Or short-circuit (e.g. serve from cache)
const cachingProxy = createProxyController({
baseURL: 'https://api.example.com',
headers: () => ({}),
beforeRequest: async (payload, req) => {
const cached = await cache.get(payload.url);
if (cached) {
return { status: 200, data: cached, headers: { 'X-Cache': 'HIT' } };
}
// returning undefined → proceeds to upstream
},
});Fires exactly once per request on all three terminal paths: upstream success, short-circuit, and error. Never throws to the caller — exceptions are swallowed and logged.
const proxy = createProxyController({
baseURL: 'https://api.example.com',
headers: () => ({}),
onResponse: (stats, req, res) => {
console.log({
url: stats.url,
method: stats.method,
status: stats.status,
durationMs: stats.durationMs,
responseSizeBytes: stats.responseSizeBytes, // from content-length header
source: stats.source, // 'upstream' | 'short-circuit'
});
},
});error.code is now more specific, making it easier to take targeted action:
error.code |
Meaning |
|---|---|
UPSTREAM_TIMEOUT |
Timeout waiting for upstream response |
UPSTREAM_UNREACHABLE |
Connection refused / DNS failure / reset |
UPSTREAM_AUTH |
Upstream returned 401 or 403 |
NETWORK_ERROR |
Other network-level failure (fallback) |
REQUEST_ERROR |
Axios request setup failure |
UNKNOWN_ERROR |
Unclassified error |
HTTP status codes are unchanged — network errors still return 503.
errorHandler: (error, req, res) => {
if (error.code === 'UPSTREAM_TIMEOUT') {
return res.status(504).json({ error: 'Gateway timeout' });
}
if (error.code === 'UPSTREAM_AUTH') {
return res.status(401).json({ error: 'Authentication required' });
}
res.status(error.status || 500).json({ error: error.message });
}- Response Errors (4xx/5xx): Server responded with error status
- Network Errors (503): No response received (timeout, connection refused)
- Request Setup Errors (500): Invalid configuration or malformed data
- Error Occurs → 2. Error Hook Processing → 3. Error Handling → 4. Fallback
const proxy = createProxyController({
baseURL: 'https://api.example.com',
errorHandlerHook: async (error, req, res) => {
// Monitor and alert
await monitoring.logError(error, { method: req.method, path: req.path });
if (error.status >= 500) {
await alerting.sendAlert({
title: 'API Proxy Error',
severity: 'high'
});
}
return error;
},
errorHandler: (error, req, res) => {
// Forward rate limiting headers
if (error.status === 429 && error.headers) {
['retry-after', 'x-ratelimit-remaining'].forEach(header => {
if (error.headers[header]) {
res.set(header, error.headers[header]);
}
});
}
res.status(error.status || 500).json({
success: false,
error: error.message,
requestId: req.headers['x-request-id']
});
}
});import {
ProxyConfig,
ProxyError,
ProxyErrorCode,
ProxyResponse,
ProxyStats,
ShortCircuitResponse,
BeforeRequestHook,
OnResponseCallback,
RequestWithLocals,
ErrorHandler,
ErrorHandlerHook,
ResponseHandler
} from 'express-simple-proxy';Passed to the onResponse callback on every terminal path.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string |
✅ | Full URL of the upstream request |
method |
string |
✅ | HTTP method (GET, POST, …) |
status |
number |
✅ | HTTP status code that was sent to the client |
durationMs |
number |
✅ | Wall-clock time from request start to terminal path (ms) |
responseSizeBytes |
number |
❌ | Parsed content-length from upstream response; omitted when header absent |
source |
'upstream' | 'short-circuit' |
✅ | 'upstream' for real HTTP calls; 'short-circuit' when beforeRequest short-circuited |
Return value from a beforeRequest hook to skip the upstream call.
| Field | Type | Required | Description |
|---|---|---|---|
status |
number |
✅ | HTTP status sent to the client |
data |
unknown |
✅ | Response body sent as JSON |
headers |
Record<string, string> |
❌ | Optional response headers |
statusText |
string |
❌ | Optional status text |
Granular error codes set on error.code:
| Code | When set |
|---|---|
UPSTREAM_TIMEOUT |
Upstream did not respond within timeout ms |
UPSTREAM_UNREACHABLE |
Connection refused or DNS failure |
UPSTREAM_AUTH |
Upstream returned 401 or 403 |
NETWORK_ERROR |
Other network-level failure (no response received) |
REQUEST_ERROR |
Axios request setup failed |
UNKNOWN_ERROR |
Unclassified error |
import {
urlJoin,
replaceUrlTemplate,
buildQueryString,
createFormDataPayload,
generateCurlCommand,
asyncWrapper,
parseSize,
resolveProxyPath,
} from 'express-simple-proxy';
// URL manipulation
const url = urlJoin('https://api.example.com', 'users', '?page=1');
const templated = replaceUrlTemplate('/users/:id', { id: 123 });
const path = resolveProxyPath('/users/:id', req.path, req.params); // → '/users/42'
// Query string building
const qs = buildQueryString({ page: 1, tags: ['red', 'blue'] });
// Parse content-length header → number | undefined
const bytes = parseSize(response.headers['content-length']);
// Form data creation
const formData = createFormDataPayload(req);
// Debug curl generation
const curlCommand = generateCurlCommand(payload, req);
// Async wrapper for middleware
const wrappedMiddleware = asyncWrapper(async (req, res, next) => {
// Your async logic
});import {
classifyResponseError,
classifyNetworkError,
isShortCircuitResponse,
buildErrorResponseBody,
filterProxyResponseHeaders,
} from 'express-simple-proxy';
// Classify an AxiosError that has a response (4xx/5xx)
// Sets error.code = 'UPSTREAM_AUTH' for 401/403
const proxyErr = classifyResponseError(axiosError);
// Classify a network-level AxiosError (no response received)
// Sets error.code = UPSTREAM_TIMEOUT | UPSTREAM_UNREACHABLE | NETWORK_ERROR
const netErr = classifyNetworkError(axiosError);
// Type guard — true if value is a ShortCircuitResponse
if (isShortCircuitResponse(hookResult)) {
res.status(hookResult.status).json(hookResult.data);
}
// Build the standard error response JSON body
const body = buildErrorResponseBody(error);
// → { error: { message, code, details? } }
// Drop content-length from a headers map (safe to forward)
const safe = filterProxyResponseHeaders(response.headers);- Total Coverage: ~93%
- Tests Passed: 183/183 ✅
- Test Suites: Unit, Integration, Utils, Errors, Omitted Path
npm test # Run all tests
npm test -- --coverage # With coverage report
npm run test:unit # Unit tests only
npm run test:integration # Integration tests only
npm run test:watch # Watch modenpm install # Install dependencies
npm run build # Build the project
npm run dev # Development mode
npm run lint # Lint code
npm run format # Format codenpm run example # Basic usage
npm run example:omitted-path # Omitted path patterns
npm run example:api-gateway # Real-world API Gateway- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- FAQ - Common questions and solutions
- Examples - Practical usage examples
- Issues - Bug reports and feature requests
MIT License - see LICENSE file for details.
Made with ❤️ by Nadim Tuhin