From ebb413d6931055b39b9b738f539c03ce9266df62 Mon Sep 17 00:00:00 2001 From: Michal Date: Sun, 18 Jan 2026 23:14:41 +0100 Subject: [PATCH] add proxy support --- README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++ cli/scripts/serve.ts | 81 +++++++++++++++++++++++++++++++++++++++++ cli/types.ts | 9 +++++ 3 files changed, 176 insertions(+) diff --git a/README.md b/README.md index 1a32131..f4dd117 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,14 @@ bootstrapApplication(AppComponent, { } } }, + "serve": { + "proxy": { + "/api/*": { + "target": "http://localhost:3000", + "changeOrigin": true + } + } + }, "environments": { "development": { "minifyNames": true, @@ -329,6 +337,84 @@ export class HighlightDirective { ## 🔧 Advanced Features +### Development Server Proxy + +The development server supports proxying HTTP requests to a backend server. This is useful when your frontend needs to communicate with an API during development. + +**Configuration in quarc.json:** + +```json +{ + "serve": { + "proxy": { + "/api/*": { + "target": "http://192.168.1.100:8080", + "changeOrigin": true + }, + "/auth/*": { + "target": "https://auth.example.com", + "changeOrigin": true, + "pathRewrite": { + "^/auth": "/api/v1/auth" + } + } + } + } +} +``` + +**Proxy Options:** + +- **Pattern matching**: Use wildcards (`*`) to match request paths + - `/api/*` matches `/api/users`, `/api/data`, etc. + - `/v1/*/data` matches `/v1/users/data`, `/v1/products/data`, etc. + +- **target**: Backend server URL (required) + - Can be HTTP or HTTPS + - Include protocol and host, optionally port + +- **changeOrigin**: Set to `true` to change the `Host` header to match the target (recommended for most cases) + +- **pathRewrite**: Object with regex patterns to rewrite request paths + - Key: regex pattern to match + - Value: replacement string + +**Example Use Cases:** + +```json +{ + "serve": { + "proxy": { + "/api/*": { + "target": "http://localhost:3000", + "changeOrigin": true + } + } + } +} +``` + +When your app makes a request to `http://localhost:4200/api/users`, it will be proxied to `http://localhost:3000/api/users`. + +**Perfect for ESP32 Development:** + +When developing for ESP32, you can proxy API requests to the device: + +```json +{ + "serve": { + "proxy": { + "/api/*": { + "target": "http://192.168.1.50", + "changeOrigin": true + } + } + } +} +``` + +This allows you to develop your frontend locally while testing against the actual ESP32 backend. + ### Lazy Loading Components are automatically lazy-loaded when using route-based code splitting: diff --git a/cli/scripts/serve.ts b/cli/scripts/serve.ts index 3585416..a3314cd 100644 --- a/cli/scripts/serve.ts +++ b/cli/scripts/serve.ts @@ -10,6 +10,7 @@ import { BaseBuilder } from './base-builder'; import { StaticPath, StaticRemotePath, + ProxyConfig, } from '../types'; class Server extends BaseBuilder { @@ -159,6 +160,82 @@ class Server extends BaseBuilder { return this.config.serve?.staticPaths || []; } + private getProxyConfig(): ProxyConfig { + return this.config.serve?.proxy || {}; + } + + private matchProxyPath(reqUrl: string): { pattern: string; config: { target: string; changeOrigin?: boolean; pathRewrite?: { [key: string]: string } } } | null { + const proxyConfig = this.getProxyConfig(); + + for (const [pattern, config] of Object.entries(proxyConfig)) { + const regexPattern = pattern.replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}`); + + if (regex.test(reqUrl)) { + return { pattern, config }; + } + } + + return null; + } + + private tryProxyRequest(reqUrl: string, req: http.IncomingMessage, res: http.ServerResponse): boolean { + const match = this.matchProxyPath(reqUrl); + + if (!match) { + return false; + } + + const { pattern, config } = match; + let targetPath = reqUrl; + + if (config.pathRewrite) { + for (const [from, to] of Object.entries(config.pathRewrite)) { + const fromRegex = new RegExp(from); + targetPath = targetPath.replace(fromRegex, to); + } + } + + const targetUrl = config.target + targetPath; + + if (this.isVerbose()) { + console.log(`[Proxy] ${req.method} ${reqUrl} -> ${targetUrl}`); + } + + const parsedUrl = new URL(targetUrl); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + const headers: http.OutgoingHttpHeaders = { ...req.headers }; + + if (config.changeOrigin) { + headers.host = parsedUrl.host; + } + + const proxyReq = protocol.request( + targetUrl, + { + method: req.method, + headers, + }, + (proxyRes) => { + if (this.isVerbose()) { + console.log(`[Proxy] Response: ${proxyRes.statusCode} for ${req.url}`); + } + res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); + proxyRes.pipe(res); + }, + ); + + proxyReq.on('error', (err) => { + console.error(`[Proxy] Error for ${req.url}:`, err.message); + res.writeHead(502); + res.end('Bad Gateway'); + }); + + req.pipe(proxyReq); + return true; + } + private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void { console.log(`[Proxy] ${req.method} ${req.url} -> ${targetUrl}`); const parsedUrl = new URL(targetUrl); @@ -245,6 +322,10 @@ class Server extends BaseBuilder { this.httpServer = http.createServer((req, res) => { const reqUrl = req.url || '/'; + if (this.tryProxyRequest(reqUrl, req, res)) { + return; + } + if (this.tryServeStaticPath(reqUrl, req, res)) { return; } diff --git a/cli/types.ts b/cli/types.ts index cd355bb..022042f 100644 --- a/cli/types.ts +++ b/cli/types.ts @@ -32,6 +32,14 @@ export interface StaticRemotePath { export type StaticPath = StaticLocalPath | StaticRemotePath; +export interface ProxyConfig { + [path: string]: { + target: string; + changeOrigin?: boolean; + pathRewrite?: { [key: string]: string }; + }; +} + export interface ActionsConfig { prebuild?: string[]; postbuild?: string[]; @@ -57,6 +65,7 @@ export interface BuildConfig { export interface ServeConfig { actions?: ActionsConfig; staticPaths?: StaticPath[]; + proxy?: ProxyConfig; } export interface QuarcConfig {