External API Proxy
Full API reference for the createExternalProxy middleware in @naap/plugin-server-sdk.
Overview#
createExternalProxy is a backend middleware factory from @naap/plugin-server-sdk that creates a transparent HTTP proxy for calling external APIs that don't support CORS. It returns an array of Express middleware handlers that you spread into your router.
import { createExternalProxy } from '@naap/plugin-server-sdk';Signature#
function createExternalProxy(config: ExternalProxyConfig): RequestHandler[]Returns [bodyParser, proxyHandler] — spread into router.post():
router.post('/my-proxy', ...createExternalProxy({ ... }));ExternalProxyConfig#
| 1 | interface ExternalProxyConfig { |
| 2 | allowedHosts: string[]; |
| 3 | targetUrlHeader?: string; |
| 4 | contentType?: string; |
| 5 | bodyLimit?: string; |
| 6 | exposeHeaders?: Array<{ from: string; to: string }>; |
| 7 | forwardHeaders?: Record<string, string> | ((req: Request) => Record<string, string>); |
| 8 | timeout?: number; |
| 9 | authorize?: (req: Request) => boolean | Promise<boolean>; |
| 10 | logger?: { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void }; |
| 11 | } |
allowedHosts#
Type: string[] | Required
Hostnames the proxy is allowed to forward to. Uses suffix matching on the target URL's hostname. This prevents SSRF attacks.
allowedHosts: ['api.stripe.com', 'livepeer.com']
// Allows: api.stripe.com, ai.livepeer.com
// Blocks: localhost, 169.254.169.254, evil.comtargetUrlHeader#
Type: string | Default: 'X-Target-URL'
The HTTP request header that contains the full URL to forward to.
targetUrlHeader: 'X-WHIP-URL'
// Frontend sends: headers: { 'X-WHIP-URL': 'https://ai.livepeer.com/...' }contentType#
Type: string | Default: 'application/json'
The content type for the proxied request and response. Determines which Express body parser to use:
'application/json'— Usesexpress.json(), forwards JSON'application/sdp'— Usesexpress.text(), forwards plain text- Any other type — Uses
express.text(), forwards plain text
contentType: 'application/sdp' // For WebRTC SDP handshakesbodyLimit#
Type: string | Default: '1mb'
Maximum allowed request body size. Uses Express size format.
bodyLimit: '5mb'exposeHeaders#
Type: Array<{ from: string; to: string }> | Default: []
Maps response headers from the external API to custom headers on the proxy response. This is needed because browsers only expose "simple" response headers by default.
| 1 | exposeHeaders: [ |
| 2 | { from: 'Location', to: 'X-WHIP-Resource' }, |
| 3 | { from: 'X-RateLimit-Remaining', to: 'X-RateLimit-Remaining' }, |
| 4 | ] |
forwardHeaders#
Type: Record<string, string> | ((req: Request) => Record<string, string>) | Default: undefined
Additional headers to include in the request to the external API. Useful for injecting API keys that should never reach the browser.
Static headers:
| 1 | forwardHeaders: { |
| 2 | 'Authorization': `Bearer ${process.env.API_KEY}`, |
| 3 | 'X-Custom': 'value', |
| 4 | } |
Dynamic headers (function):
forwardHeaders: (req) => ({
'Authorization': `Bearer ${getApiKeyForUser(req)}`,
})timeout#
Type: number | Default: 30000
Request timeout in milliseconds. If the external API doesn't respond within this time, the proxy returns a 504 Gateway Timeout.
timeout: 60_000 // 60 seconds for slow APIsauthorize#
Type: (req: Request) => boolean | Promise<boolean> | Default: undefined
Optional authorization callback. Called before the proxy forwards the request. Return false or throw an error to deny the request with a 403 Forbidden.
| 1 | authorize: async (req) => { |
| 2 | const userId = getUserId(req); |
| 3 | const apiKey = await getUserApiKey(userId); |
| 4 | return !!apiKey; // Only proxy if user has an API key |
| 5 | } |
logger#
Type: { info: Function; error: Function } | Default: console
Custom logger for proxy events. The proxy logs:
info: When forwarding a request (hostname + path only)error: When the external API returns an error or the proxy fails
| 1 | logger: { |
| 2 | info: (...args) => winston.info('[proxy]', ...args), |
| 3 | error: (...args) => winston.error('[proxy]', ...args), |
| 4 | } |
Response Format#
Success#
When the external API returns a successful response, the proxy returns:
- Status: The external API's status code (typically 200)
- Content-Type: Matches the configured
contentType - Body: The external API's response body (JSON or text, depending on content type)
- Headers: Any mapped
exposeHeaders
Error Responses#
All error responses follow the standard NaaP format:
| 1 | { |
| 2 | "success": false, |
| 3 | "error": { |
| 4 | "message": "Description of the error" |
| 5 | } |
| 6 | } |
| Status | Condition |
|---|---|
| 400 | Missing target URL header, invalid URL, blocked host, empty body |
| 403 | authorize callback returned false |
| 4xx/5xx | Forwarded from external API |
| 504 | Request timed out |
| 500 | Internal proxy error |
CORS Headers#
The proxy itself doesn't need special CORS configuration — it runs on your plugin backend which already has CORS configured by createPluginServer. The X-WHIP-URL and similar custom headers are included in @naap/types CORS_ALLOWED_HEADERS.
If you use a custom targetUrlHeader, make sure it's listed in CORS_ALLOWED_HEADERS in @naap/types/src/http-headers.ts.
Complete Example#
| 1 | // Backend: proxy Stripe API calls |
| 2 | import { createPluginServer, createExternalProxy } from '@naap/plugin-server-sdk'; |
| 3 | |
| 4 | const { router, start } = createPluginServer({ |
| 5 | name: 'payment-plugin', |
| 6 | port: 4020, |
| 7 | }); |
| 8 | |
| 9 | router.post( |
| 10 | '/payments/stripe-proxy', |
| 11 | ...createExternalProxy({ |
| 12 | allowedHosts: ['api.stripe.com'], |
| 13 | targetUrlHeader: 'X-Target-URL', |
| 14 | contentType: 'application/json', |
| 15 | timeout: 15_000, |
| 16 | forwardHeaders: { |
| 17 | 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, |
| 18 | }, |
| 19 | authorize: async (req) => { |
| 20 | const user = (req as any).user; |
| 21 | return !!user?.id; |
| 22 | }, |
| 23 | logger: { |
| 24 | info: (...args) => console.log('[stripe-proxy]', ...args), |
| 25 | error: (...args) => console.error('[stripe-proxy]', ...args), |
| 26 | }, |
| 27 | }) |
| 28 | ); |
| 29 | |
| 30 | start(); |
| 1 | // Frontend: call Stripe through the proxy |
| 2 | import { getPluginBackendUrl } from '@naap/plugin-sdk'; |
| 3 | |
| 4 | async function createPaymentIntent(amount: number) { |
| 5 | const proxyUrl = getPluginBackendUrl('payment-plugin', { |
| 6 | apiPath: '/api/v1/payments/stripe-proxy', |
| 7 | }); |
| 8 | |
| 9 | const res = await fetch(proxyUrl, { |
| 10 | method: 'POST', |
| 11 | headers: { |
| 12 | 'Content-Type': 'application/json', |
| 13 | 'X-Target-URL': 'https://api.stripe.com/v1/payment_intents', |
| 14 | 'Authorization': `Bearer ${authToken}`, |
| 15 | }, |
| 16 | body: JSON.stringify({ amount, currency: 'usd' }), |
| 17 | }); |
| 18 | |
| 19 | return res.json(); |
| 20 | } |
Related#
- Proxying External APIs Guide — Tutorial with step-by-step walkthrough
- Backend Development — General backend setup
- SDK Hooks — Frontend hooks including
getPluginBackendUrl - API Integration Example — More API integration patterns