@lopatnov/express-reverse-proxy 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,14 +18,44 @@
18
18
  - [Demo](#demo)
19
19
  - [How It Works](#how-it-works)
20
20
  - [CLI Options](#cli-options)
21
+ - [--config](#--config)
22
+ - [--init](#--init)
23
+ - [--cluster](#--cluster)
24
+ - [--cluster-config](#--cluster-config)
21
25
  - [Configuration](#configuration)
22
26
  - [port](#port)
27
+ - [logging](#logging)
28
+ - [hotReload](#hotreload)
29
+ - [compression](#compression)
30
+ - [helmet](#helmet)
31
+ - [cors](#cors)
32
+ - [favicon](#favicon)
33
+ - [responseTime](#responsetime)
34
+ - [rateLimit](#ratelimit)
35
+ - [basicAuth](#basicauth)
36
+ - [healthCheck](#healthcheck)
37
+ - [cgi](#cgi)
38
+ - [upload](#upload)
23
39
  - [headers](#headers)
40
+ - [redirects](#redirects)
24
41
  - [folders](#folders)
25
42
  - [proxy](#proxy)
26
43
  - [unhandled](#unhandled)
27
44
  - [host](#host)
45
+ - [ssl](#ssl)
28
46
  - [Configuration Recipes](#configuration-recipes)
47
+ - [Static files + proxy fallback](#static-files-first-then-fall-back-to-back-end)
48
+ - [Static files + API on a specific path](#static-files--api-on-a-specific-path)
49
+ - [Hot reload dev server](#hot-reload-dev-server)
50
+ - [HTTPS with automatic HTTP redirect](#https-with-automatic-http-redirect)
51
+ - [HTTPS with a self-signed certificate](#https-with-a-self-signed-certificate-local-dev)
52
+ - [Production hardening](#production-hardening-helmet--cors--compression)
53
+ - [Protected admin area](#protected-admin-area)
54
+ - [URL migration](#url-migration-permanent-redirects)
55
+ - [Load-balanced API proxy](#load-balanced-api-proxy)
56
+ - [File upload server](#file-upload-server)
57
+ - [Health check for Docker / Kubernetes](#health-check-for-docker--kubernetes)
58
+ - [CORS headers + rich error responses](#cors-headers--rich-error-responses)
29
59
  - [Docker & PM2](#docker--pm2)
30
60
  - [Testing](#testing)
31
61
  - [Troubleshooting](#troubleshooting)
@@ -59,7 +89,13 @@ npx @lopatnov/express-reverse-proxy
59
89
 
60
90
  ## Quick Start
61
91
 
62
- 1. Create a `server-config.json` in your project root:
92
+ 1. Generate a `server-config.json` interactively (or create it manually):
93
+
94
+ ```shell
95
+ express-reverse-proxy --init
96
+ ```
97
+
98
+ Or create it manually:
63
99
 
64
100
  ```json
65
101
  {
@@ -131,41 +167,111 @@ Browser
131
167
 
132
168
  ## How It Works
133
169
 
134
- Every incoming request is processed in order:
170
+ Every incoming request passes through the middleware chain in this order:
135
171
 
136
172
  ```
137
173
  Request
138
174
 
139
- ├─▶ Custom headers applied (if configured)
175
+ ├─▶ Logging (morgan) — log to console or file
176
+ ├─▶ responseTime — measure and record latency
177
+ ├─▶ cors — CORS headers + preflight OPTIONS
178
+ ├─▶ compression — gzip/deflate response body
179
+ ├─▶ helmet — security HTTP headers
180
+ ├─▶ favicon — serve /favicon.ico from memory
181
+
182
+ ├─▶ healthCheck — GET /__health__ → {status, uptime}
183
+ │ └─▶ Path matches → respond immediately (bypasses auth below)
184
+
185
+ ├─▶ rateLimit — 429 if client over limit
186
+ ├─▶ basicAuth — 401 if credentials missing/wrong
187
+
188
+ ├─▶ Custom headers applied (headers)
189
+
190
+ ├─▶ Redirect rules checked (redirects)
191
+ │ └─▶ Path matches → 301/302 redirect
140
192
 
141
193
  ├─▶ Static files checked (folders, in order)
142
194
  │ └─▶ File found → serve it
143
195
 
196
+ ├─▶ CGI scripts checked (cgi)
197
+ │ └─▶ Path + extension matches → execute script → stream response
198
+
199
+ ├─▶ File upload handler (upload)
200
+ │ └─▶ POST path matches → save files, return JSON
201
+ │ GET path matches → serve uploaded file
202
+
144
203
  ├─▶ Reverse proxy rules checked (proxy)
145
204
  │ └─▶ Path matches → forward to back-end → return response
205
+ │ (round-robin if multiple targets configured)
146
206
 
147
207
  └─▶ No match → unhandled handler (by Accept header)
148
- └─▶ Return status + body or redirect
208
+ └─▶ Return status + body
149
209
  ```
150
210
 
151
- Static files always take priority over proxy rules. Proxies are checked only when no file matches.
211
+ Options that are not configured are skipped entirely. Static files always take priority over proxy rules.
152
212
 
153
213
  ---
154
214
 
155
215
  ## CLI Options
156
216
 
217
+ The package installs two equivalent commands — use whichever you prefer:
218
+
219
+ ```shell
220
+ express-reverse-proxy [options]
221
+ lerp [options]
222
+ ```
223
+
224
+ `lerp` is a short alias for **L**opatnov **E**xpress **R**everse **P**roxy.
225
+
157
226
  | Option | Description |
158
227
  | ------------------------- | ----------------------------------------------------------------------------------------------- |
159
228
  | `--help` | Print help and exit |
160
229
  | `--config <file>` | Path to the JSON configuration file. Default: `server-config.json` |
230
+ | `--init` | Interactively create a `server-config.json` in the current directory |
161
231
  | `--cluster [action]` | Manage the PM2 cluster. Action defaults to `start` when omitted |
162
232
  | `--cluster-config <file>` | Path to a custom PM2 ecosystem config file. Default: `ecosystem.config.cjs` next to `server.js` |
163
233
 
164
- ### --cluster actions
234
+ ### --config
235
+
236
+ Specify the path to the configuration file. Accepts a file path or a directory (in which case `server-config.json` inside that directory is used).
237
+
238
+ ```shell
239
+ express-reverse-proxy --config ./configs/prod.json
240
+ express-reverse-proxy --config ./configs/
241
+ ```
242
+
243
+ Default: `server-config.json` in the current working directory.
244
+
245
+ If the file is not found and `--config` was explicitly provided, the server exits with an error. If no `--config` is given and the default file is missing, the server starts with built-in defaults (`port: 8000`, `folders: "."`) and prints a warning.
246
+
247
+ ### --init
248
+
249
+ Interactively creates a `server-config.json` in the current working directory. Asks for port, static folder, optional proxy target, and hot reload preference.
250
+
251
+ ```shell
252
+ express-reverse-proxy --init
253
+ ```
254
+
255
+ Example session:
256
+
257
+ ```
258
+ Port [8000]: 8080
259
+ Static folder [.]: www
260
+ Proxy path (e.g. /api) [skip]: /api
261
+ Proxy target for /api: http://localhost:4000
262
+ Hot reload? [y/N]: y
263
+ [init] Created /your/project/server-config.json
264
+ ```
265
+
266
+ If `server-config.json` already exists, the command asks before overwriting.
267
+
268
+ ### --cluster
269
+
270
+ Manage the PM2 process cluster. Action defaults to `start` when omitted.
165
271
 
166
272
  | Action | Description |
167
273
  | --------- | ------------------------------------------------------ |
168
- | `start` | Start the PM2 cluster (default when action is omitted) |
274
+ | `start` | Start the cluster (default when action is omitted) |
169
275
  | `stop` | Stop all cluster instances |
170
276
  | `restart` | Restart all cluster instances |
171
277
  | `status` | Show PM2 process status table |
@@ -173,7 +279,7 @@ Static files always take priority over proxy rules. Proxies are checked only whe
173
279
  | `monitor` | Open the PM2 real-time monitor |
174
280
 
175
281
  ```shell
176
- express-reverse-proxy --cluster
282
+ express-reverse-proxy --cluster # same as --cluster start
177
283
  express-reverse-proxy --cluster start
178
284
  express-reverse-proxy --cluster stop
179
285
  express-reverse-proxy --cluster restart
@@ -182,25 +288,41 @@ express-reverse-proxy --cluster logs
182
288
  express-reverse-proxy --cluster monitor
183
289
  ```
184
290
 
185
- Pass a custom config to cluster workers with `--config`:
291
+ Pass `--config` to forward a custom config path to all cluster workers:
186
292
 
187
293
  ```shell
188
294
  express-reverse-proxy --cluster start --config ./configs/prod.json
189
295
  ```
190
296
 
191
- Use a custom PM2 ecosystem file with `--cluster-config`:
297
+ ### --cluster-config
298
+
299
+ Override the PM2 ecosystem config file used to start the cluster. Default: `ecosystem.config.cjs` in the same directory as `server.js`.
192
300
 
193
301
  ```shell
194
302
  express-reverse-proxy --cluster start --cluster-config ./my-ecosystem.config.cjs
195
303
  express-reverse-proxy --cluster restart --cluster-config /etc/myapp/ecosystem.config.cjs
196
304
  ```
197
305
 
306
+ Useful when you need custom PM2 settings such as a different number of instances, environment variables, or log file paths. See [PM2 ecosystem documentation](https://pm2.keymetrics.io/docs/usage/application-declaration/) for all options.
307
+
198
308
  ---
199
309
 
200
310
  ## Configuration
201
311
 
202
312
  All configuration lives in a single JSON file (default `server-config.json`).
203
313
 
314
+ ### IDE autocomplete (JSON Schema)
315
+
316
+ Add a `$schema` reference to your config file to get property autocomplete, descriptions, and type checking in VS Code and other editors:
317
+
318
+ ```json
319
+ {
320
+ "$schema": "https://unpkg.com/@lopatnov/express-reverse-proxy/server-config.schema.json",
321
+ "port": 8080,
322
+ "folders": "www"
323
+ }
324
+ ```
325
+
204
326
  ### Environment variables
205
327
 
206
328
  | Variable | Default | Description |
@@ -220,7 +342,7 @@ The port the server listens on. Defaults to `8000`. Can also be set via the `POR
220
342
 
221
343
  ### logging
222
344
 
223
- Controls HTTP request logging (Morgan). Enabled by default. Set to `false` to silence per-request log lines — useful in production behind another proxy, or to keep console output clean.
345
+ Controls HTTP request logging (Morgan). Enabled by default (`dev` format). Set to `false` to silence per-request log lines — useful in production behind another proxy, or to keep console output clean.
224
346
 
225
347
  ```json
226
348
  {
@@ -230,6 +352,23 @@ Controls HTTP request logging (Morgan). Enabled by default. Set to `false` to si
230
352
  }
231
353
  ```
232
354
 
355
+ **Object form** — write logs to a file and/or choose a different format:
356
+
357
+ ```json
358
+ {
359
+ "logging": { "format": "combined", "file": "./logs/access.log" }
360
+ }
361
+ ```
362
+
363
+ | Option | Default | Description |
364
+ | -------- | ------------ | -------------------------------------------------------------- |
365
+ | `format` | `"combined"` | Morgan format: `combined`, `common`, `dev`, `short`, or `tiny` |
366
+ | `file` | none | Path to log file (relative to config file). Appended if exists |
367
+
368
+ When `file` is set, logs are written to the file only (not to the console).
369
+
370
+ > Logging is applied per-site, so each virtual host can have its own format and log file.
371
+
233
372
  ### hotReload
234
373
 
235
374
  Watches the `folders` directories for file changes and automatically reloads connected browser tabs. Uses Server-Sent Events (SSE). Intended for local development only.
@@ -279,6 +418,42 @@ Add headers to every response — useful for CORS in development.
279
418
  }
280
419
  ```
281
420
 
421
+ ### redirects
422
+
423
+ Permanently or temporarily redirect URL paths to new destinations. Redirects are checked before static files and proxy rules.
424
+
425
+ **Object form** — map source paths to destinations:
426
+
427
+ ```json
428
+ {
429
+ "redirects": {
430
+ "/old-path": "/new-path",
431
+ "/legacy": "https://new.example.com",
432
+ "/temp": { "to": "/temporary-destination", "status": 302 }
433
+ }
434
+ }
435
+ ```
436
+
437
+ **Array form** — explicit entries with `from`, `to`, and optional `status`:
438
+
439
+ ```json
440
+ {
441
+ "redirects": [
442
+ { "from": "/old", "to": "/new" },
443
+ { "from": "/moved", "to": "https://example.com", "status": 301 },
444
+ { "from": "/temp", "to": "/somewhere", "status": 302 }
445
+ ]
446
+ }
447
+ ```
448
+
449
+ | Field | Default | Description |
450
+ | -------- | ------- | -------------------------------------------------------- |
451
+ | `from` | — | Source URL path *(array form only, required)* |
452
+ | `to` | — | Destination path or full URL *(required)* |
453
+ | `status` | `301` | HTTP redirect status: `301`, `302`, `307`, or `308` |
454
+
455
+ > `301` — Moved Permanently. `302` — Found (temporary). Use `301` for permanent URL changes and `302` for temporary ones.
456
+
282
457
  ### folders
283
458
 
284
459
  Serve static files. Supports three forms:
@@ -358,6 +533,18 @@ Forward requests to a back-end server. Supports three forms:
358
533
  }
359
534
  ```
360
535
 
536
+ **Load balancing** — pass an array of targets for a path to distribute requests in round-robin:
537
+
538
+ ```json
539
+ {
540
+ "proxy": {
541
+ "/api": ["http://backend1:3000", "http://backend2:3000", "http://backend3:3000"]
542
+ }
543
+ }
544
+ ```
545
+
546
+ Requests to `/api` are forwarded to the backends in turn: `backend1`, `backend2`, `backend3`, `backend1`, …
547
+
361
548
  ### unhandled
362
549
 
363
550
  Control responses when no static file or proxy rule matches. Rules are selected by the request's `Accept` header.
@@ -438,11 +625,12 @@ To use multi-site mode, make the config file an **array** instead of an object.
438
625
 
439
626
  Enable HTTPS on a port by adding an `ssl` object to any site config for that port. All sites sharing the same port use the same certificate.
440
627
 
441
- | Field | Type | Description |
442
- | ------ | -------- | -------------------------------------------------------- |
443
- | `key` | `string` | Path to the private key file (PEM format) |
444
- | `cert` | `string` | Path to the certificate file (PEM format) |
445
- | `ca` | `string` | *(optional)* Path to the CA bundle for client validation |
628
+ | Field | Type | Description |
629
+ | ---------- | --------- | -------------------------------------------------------- |
630
+ | `key` | `string` | Path to the private key file (PEM format) |
631
+ | `cert` | `string` | Path to the certificate file (PEM format) |
632
+ | `ca` | `string` | *(optional)* Path to the CA bundle for client validation |
633
+ | `redirect` | `integer` | *(optional)* HTTP port to redirect (301) to HTTPS |
446
634
 
447
635
  Paths are resolved **relative to the config file**, not the current working directory.
448
636
 
@@ -460,8 +648,363 @@ Paths are resolved **relative to the config file**, not the current working dire
460
648
  }
461
649
  ```
462
650
 
651
+ **Automatic HTTP → HTTPS redirect** — set `redirect` to the HTTP port to also listen on plain HTTP and redirect all traffic to HTTPS:
652
+
653
+ ```json
654
+ {
655
+ "port": 443,
656
+ "ssl": {
657
+ "key": "./certs/key.pem",
658
+ "cert": "./certs/cert.pem",
659
+ "redirect": 80
660
+ },
661
+ "folders": "./public"
662
+ }
663
+ ```
664
+
665
+ This starts an HTTPS server on port `443` and a tiny redirect-only HTTP server on port `80`. All `http://` requests are permanently redirected (301) to `https://`.
666
+
463
667
  > All site configs on the same port must either all have `ssl` or none — mixing is a startup error.
464
668
 
669
+ ### compression
670
+
671
+ Enable gzip/deflate response compression. Reduces the size of HTML, CSS, JS, and JSON responses sent to the browser. Set to `true` for defaults, or pass an options object.
672
+
673
+ ```json
674
+ {
675
+ "port": 8080,
676
+ "compression": true,
677
+ "folders": "www"
678
+ }
679
+ ```
680
+
681
+ With custom options (see [compression docs](https://github.com/expressjs/compression#options)):
682
+
683
+ ```json
684
+ {
685
+ "compression": { "level": 6, "threshold": 1024 }
686
+ }
687
+ ```
688
+
689
+ > Compression is applied per-site. Assets that are already compressed (images, fonts, video) are not affected — the browser signals it accepts compressed responses via the `Accept-Encoding` header.
690
+
691
+ ### helmet
692
+
693
+ Set security-related HTTP response headers. Protects against common web vulnerabilities by configuring headers such as `Content-Security-Policy`, `X-Frame-Options`, `Strict-Transport-Security`, and others.
694
+
695
+ ```json
696
+ {
697
+ "port": 8080,
698
+ "helmet": true,
699
+ "folders": "www"
700
+ }
701
+ ```
702
+
703
+ Disable a specific header (see [helmet docs](https://helmetjs.github.io/) for all options):
704
+
705
+ ```json
706
+ {
707
+ "helmet": { "contentSecurityPolicy": false }
708
+ }
709
+ ```
710
+
711
+ > When `helmet: true` is set, the default helmet configuration is applied. This may block inline scripts and cross-origin resources. Adjust `contentSecurityPolicy` or other options as needed for your project.
712
+
713
+ ### cors
714
+
715
+ Enable CORS (Cross-Origin Resource Sharing) headers and handle preflight `OPTIONS` requests automatically. Useful when your front-end on one origin calls an API on a different origin.
716
+
717
+ ```json
718
+ {
719
+ "port": 8080,
720
+ "cors": true,
721
+ "proxy": { "/api": "http://localhost:4000" }
722
+ }
723
+ ```
724
+
725
+ Restrict to a specific origin (see [cors docs](https://github.com/expressjs/cors#configuration-options)):
726
+
727
+ ```json
728
+ {
729
+ "cors": { "origin": "https://app.example.com" }
730
+ }
731
+ ```
732
+
733
+ > The `cors` middleware handles `OPTIONS` preflight requests that the `headers` option cannot respond to. Use `cors` when you need to allow requests from JavaScript on a different domain — for example a React app calling this proxy's API routes.
734
+
735
+ ### favicon
736
+
737
+ Serve a favicon file efficiently. The file is read into memory at startup and served from there on every `/favicon.ico` request — before static folder scanning or proxy rules run.
738
+
739
+ ```json
740
+ {
741
+ "port": 8080,
742
+ "favicon": "./public/favicon.ico",
743
+ "folders": "www"
744
+ }
745
+ ```
746
+
747
+ The path is resolved **relative to the config file**, consistent with the `ssl` option. Absolute paths are also accepted.
748
+
749
+ > If your favicon already lives inside a directory listed in `folders`, this option is not needed — `express.static` will serve it automatically.
750
+
751
+ ### responseTime
752
+
753
+ Add an `X-Response-Time` header to every response, recording how long the server took to handle the request. Useful for performance monitoring and debugging.
754
+
755
+ ```json
756
+ {
757
+ "port": 8080,
758
+ "responseTime": true,
759
+ "folders": "www"
760
+ }
761
+ ```
762
+
763
+ With custom precision (see [response-time docs](https://github.com/expressjs/response-time#options)):
764
+
765
+ ```json
766
+ {
767
+ "responseTime": { "digits": 0, "suffix": false }
768
+ }
769
+ ```
770
+
771
+ ### rateLimit
772
+
773
+ Limit the number of requests a client can make in a time window. Responds with `429 Too Many Requests` when the limit is exceeded. Useful when running without a dedicated reverse proxy.
774
+
775
+ ```json
776
+ {
777
+ "port": 8080,
778
+ "rateLimit": { "windowMs": 60000, "limit": 100 },
779
+ "folders": "www"
780
+ }
781
+ ```
782
+
783
+ | Option | Default | Description |
784
+ | ----------- | -------- | ------------------------------------------------ |
785
+ | `windowMs` | `60000` | Time window in milliseconds |
786
+ | `limit` | `5` | Maximum requests per client per window |
787
+ | `message` | built-in | Response body when limit is exceeded |
788
+
789
+ See [express-rate-limit docs](https://express-rate-limit.mintlify.app/reference/configuration) for all options.
790
+
791
+ > Rate limiting is applied per-site and per IP address. In production behind Nginx or Caddy, configure rate limiting there instead — it runs before Node.js and is more efficient.
792
+
793
+ ### basicAuth
794
+
795
+ Protect the site with HTTP Basic Authentication. All requests must include valid credentials or the server responds with `401 Unauthorized`.
796
+
797
+ ```json
798
+ {
799
+ "port": 8080,
800
+ "basicAuth": {
801
+ "users": { "admin": "s3cr3t" },
802
+ "challenge": true
803
+ },
804
+ "folders": "www"
805
+ }
806
+ ```
807
+
808
+ | Option | Default | Description |
809
+ | ----------- | ------- | ------------------------------------------------------------ |
810
+ | `users` | — | Object mapping username → password *(required)* |
811
+ | `challenge` | `false` | Send `WWW-Authenticate` header to trigger browser login dialog |
812
+ | `realm` | — | Realm string shown in the browser login dialog |
813
+
814
+ See [express-basic-auth docs](https://github.com/LionC/express-basic-auth#options) for all options.
815
+
816
+ > Passwords are compared in plain text. Do not use Basic Auth over plain HTTP in production — always combine with `ssl` or put behind a TLS-terminating proxy.
817
+
818
+ ### healthCheck
819
+
820
+ Expose a lightweight health check endpoint. Returns a JSON response with server status, uptime, and current timestamp. Useful for load balancers, monitoring systems, and container health checks.
821
+
822
+ ```json
823
+ {
824
+ "port": 8080,
825
+ "healthCheck": true,
826
+ "folders": "www"
827
+ }
828
+ ```
829
+
830
+ Default endpoint: `GET /__health__`
831
+
832
+ ```json
833
+ { "status": "ok", "uptime": 42.3, "timestamp": "2026-01-01T12:00:00.000Z" }
834
+ ```
835
+
836
+ Custom path:
837
+
838
+ ```json
839
+ {
840
+ "healthCheck": { "path": "/health" }
841
+ }
842
+ ```
843
+
844
+ | Option | Default | Description |
845
+ | ------ | -------------- | -------------------------------- |
846
+ | `path` | `"/__health__"` | URL path of the health endpoint |
847
+
848
+ > The health check endpoint is placed before rate limiting and basic auth — it is always publicly accessible regardless of other authentication settings.
849
+
850
+ ### cgi
851
+
852
+ Execute server-side scripts using the CGI (Common Gateway Interface) protocol. When a request matches the configured URL prefix and file extension, the script is spawned as a child process — HTTP headers become environment variables, the request body is piped to stdin, and the script's stdout is streamed back as the HTTP response.
853
+
854
+ ```json
855
+ {
856
+ "port": 8080,
857
+ "cgi": {
858
+ "path": "/cgi-bin",
859
+ "dir": "./cgi-bin",
860
+ "extensions": [".cgi", ".pl", ".py", ".sh"],
861
+ "interpreters": {
862
+ ".py": "python3",
863
+ ".sh": "sh",
864
+ ".pl": "perl"
865
+ }
866
+ }
867
+ }
868
+ ```
869
+
870
+ | Option | Default | Description |
871
+ | -------------- | ------------------------------------- | -------------------------------------------------------------------- |
872
+ | `path` | `"/cgi-bin"` | URL prefix that triggers CGI dispatch |
873
+ | `dir` | `"./cgi-bin"` | Local directory containing scripts (resolved relative to config file)|
874
+ | `extensions` | `[".cgi", ".pl", ".py", ".sh"]` | File extensions treated as executable CGI scripts |
875
+ | `interpreters` | `{}` | Map of file extension → interpreter command |
876
+
877
+ Shorthand — point directly to the script directory (all defaults apply):
878
+
879
+ ```json
880
+ {
881
+ "cgi": "./cgi-bin"
882
+ }
883
+ ```
884
+
885
+ CGI environment variables set for every request:
886
+
887
+ | Variable | Value |
888
+ | ----------------- | -------------------------------------------------------- |
889
+ | `REQUEST_METHOD` | HTTP method (`GET`, `POST`, …) |
890
+ | `QUERY_STRING` | URL query string (without `?`) |
891
+ | `CONTENT_TYPE` | `Content-Type` request header |
892
+ | `CONTENT_LENGTH` | `Content-Length` request header |
893
+ | `SCRIPT_FILENAME` | Absolute path to the script file |
894
+ | `SCRIPT_NAME` | URL path to the script (e.g. `/cgi-bin/hello.py`) |
895
+ | `SERVER_NAME` | Requested hostname |
896
+ | `SERVER_PORT` | Server listen port |
897
+ | `REMOTE_ADDR` | Client IP address |
898
+ | `HTTP_*` | All request headers (e.g. `HTTP_ACCEPT`, `HTTP_HOST`) |
899
+
900
+ A minimal Python example (`cgi-bin/hello.py`):
901
+
902
+ ```python
903
+ #!/usr/bin/env python3
904
+ print("Content-Type: text/plain")
905
+ print("Status: 200 OK")
906
+ print()
907
+ print("Hello from CGI!")
908
+ ```
909
+
910
+ > **Unix/macOS note:** Scripts must be executable: `chmod +x cgi-bin/hello.py`. Alternatively, configure an `interpreters` entry for the extension — no executable bit required when an interpreter is specified.
911
+
912
+ > **Windows note:** Scripts are not directly executable on Windows. You must configure `interpreters` for every extension you use; otherwise the request returns a `500` spawn error.
913
+
914
+ **Array form** — multiple independent CGI directories on the same site:
915
+
916
+ ```json
917
+ {
918
+ "cgi": [
919
+ {
920
+ "path": "/py-scripts",
921
+ "dir": "./py-scripts",
922
+ "extensions": [".py"],
923
+ "interpreters": { ".py": "python3" }
924
+ },
925
+ {
926
+ "path": "/node-scripts",
927
+ "dir": "./node-scripts",
928
+ "extensions": [".js"],
929
+ "interpreters": { ".js": "node" }
930
+ }
931
+ ]
932
+ }
933
+ ```
934
+
935
+ Each entry in the array sets up an independent CGI mount point with its own directory, URL prefix, extensions, and interpreters.
936
+
937
+ ### upload
938
+
939
+ Accept file uploads via `multipart/form-data` and save them to a local directory. Uploaded files can be retrieved immediately via `GET`.
940
+
941
+ ```json
942
+ {
943
+ "port": 8080,
944
+ "upload": {
945
+ "path": "/upload",
946
+ "dir": "./uploads",
947
+ "maxFileSize": 10485760,
948
+ "maxFiles": 10,
949
+ "allowedTypes": ["image/jpeg", "image/png", "application/pdf"],
950
+ "fieldName": "file"
951
+ }
952
+ }
953
+ ```
954
+
955
+ Shorthand — directory only (all defaults apply):
956
+
957
+ ```json
958
+ {
959
+ "upload": "./uploads"
960
+ }
961
+ ```
962
+
963
+ | Option | Default | Description |
964
+ | --------------- | -------------- | ------------------------------------------------------------------------ |
965
+ | `path` | `"/upload"` | URL prefix for the upload endpoint |
966
+ | `dir` | `"./uploads"` | Save directory (resolved relative to the config file) |
967
+ | `maxFileSize` | none | Maximum file size in bytes; responds with `413` when exceeded |
968
+ | `maxFiles` | none | Maximum number of files per request; responds with `400` when exceeded |
969
+ | `allowedTypes` | none | MIME type whitelist; responds with `400` when the type is not in the list|
970
+ | `fieldName` | any field | Accept only files uploaded in this specific form field |
971
+
972
+ **Array form** — multiple upload endpoints on the same site:
973
+
974
+ ```json
975
+ {
976
+ "upload": [
977
+ { "path": "/photos", "dir": "./photos", "allowedTypes": ["image/jpeg", "image/png"] },
978
+ { "path": "/docs", "dir": "./documents", "allowedTypes": ["application/pdf"], "maxFileSize": 5242880 }
979
+ ]
980
+ }
981
+ ```
982
+
983
+ **HTTP interface:**
984
+
985
+ | Method | URL | Description |
986
+ | ------ | ---------------- | ---------------------------------------- |
987
+ | `POST` | `<path>` | Upload files via `multipart/form-data` |
988
+ | `GET` | `<path>/<name>` | Retrieve a previously uploaded file |
989
+
990
+ `POST` success response (`200`):
991
+
992
+ ```json
993
+ {
994
+ "files": [
995
+ { "file": "photo-1700000000000-123456789.jpg", "size": 45678, "originalName": "photo.jpg" }
996
+ ]
997
+ }
998
+ ```
999
+
1000
+ Upload with curl:
1001
+
1002
+ ```shell
1003
+ curl -F "file=@photo.jpg" http://localhost:8080/upload
1004
+ ```
1005
+
1006
+ > The upload directory is created automatically at startup if it does not exist. Saved filenames include a timestamp and random suffix to avoid collisions.
1007
+
465
1008
  ---
466
1009
 
467
1010
  ## Configuration Recipes
@@ -499,6 +1042,65 @@ Only `/api/*` requests go to the back-end; everything else stays local.
499
1042
  - `GET /api/users` → proxied to `http://localhost:4000/users`
500
1043
  - `GET /missing` → 404 Not Found
501
1044
 
1045
+ ### Hot reload dev server
1046
+
1047
+ Local development setup: serve a front-end build folder, proxy API requests to a local back-end, and automatically reload the browser on file changes.
1048
+
1049
+ ```json
1050
+ {
1051
+ "port": 3000,
1052
+ "hotReload": true,
1053
+ "folders": "./dist",
1054
+ "proxy": {
1055
+ "/api": "http://localhost:4000"
1056
+ }
1057
+ }
1058
+ ```
1059
+
1060
+ Add the client script to your HTML (or import it in your bundler entry point):
1061
+
1062
+ ```html
1063
+ <script src="/__hot-reload__/client.js"></script>
1064
+ ```
1065
+
1066
+ The browser reconnects automatically after server restarts.
1067
+
1068
+ ---
1069
+
1070
+ ### HTTPS with automatic HTTP redirect
1071
+
1072
+ Serve the site over HTTPS and redirect all plain-HTTP traffic (port 80) to HTTPS (port 443) with a permanent 301 redirect.
1073
+
1074
+ ```shell
1075
+ mkdir certs
1076
+ openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
1077
+ -days 365 -nodes -subj "/CN=example.com"
1078
+ ```
1079
+
1080
+ ```json
1081
+ {
1082
+ "port": 443,
1083
+ "ssl": {
1084
+ "key": "./certs/key.pem",
1085
+ "cert": "./certs/cert.pem",
1086
+ "redirect": 80
1087
+ },
1088
+ "folders": "./public",
1089
+ "proxy": {
1090
+ "/api": "http://localhost:4000"
1091
+ }
1092
+ }
1093
+ ```
1094
+
1095
+ The server logs two listeners on startup:
1096
+
1097
+ ```
1098
+ [listen] https://localhost:443
1099
+ [listen] http redirect :80 → https :443
1100
+ ```
1101
+
1102
+ ---
1103
+
502
1104
  ### HTTPS with a self-signed certificate (local dev)
503
1105
 
504
1106
  ```shell
@@ -524,12 +1126,186 @@ openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
524
1126
  Start and open in browser (accept the self-signed cert warning):
525
1127
 
526
1128
  ```shell
527
- node server.js --config server-config.json
1129
+ express-reverse-proxy --config server-config.json
528
1130
  # [listen] https://localhost:8443
529
1131
  ```
530
1132
 
531
1133
  ---
532
1134
 
1135
+ ### Production hardening (helmet + cors + compression)
1136
+
1137
+ Enable security headers, CORS, and response compression in one config:
1138
+
1139
+ ```json
1140
+ {
1141
+ "port": 8080,
1142
+ "compression": true,
1143
+ "helmet": true,
1144
+ "cors": { "origin": "https://app.example.com" },
1145
+ "responseTime": true,
1146
+ "folders": "www",
1147
+ "proxy": {
1148
+ "/api": "http://localhost:4000"
1149
+ }
1150
+ }
1151
+ ```
1152
+
1153
+ ---
1154
+
1155
+ ### Protected admin area
1156
+
1157
+ Protect a site with rate limiting and HTTP Basic Auth. Useful for internal tools or staging environments.
1158
+
1159
+ ```json
1160
+ {
1161
+ "port": 8080,
1162
+ "rateLimit": { "windowMs": 60000, "limit": 30 },
1163
+ "basicAuth": {
1164
+ "users": { "admin": "s3cr3t", "viewer": "readonly" },
1165
+ "challenge": true
1166
+ },
1167
+ "folders": "./admin",
1168
+ "proxy": {
1169
+ "/api": "http://localhost:4000"
1170
+ }
1171
+ }
1172
+ ```
1173
+
1174
+ - Requests without valid credentials → `401 Unauthorized` (browser shows login dialog)
1175
+ - More than 30 requests per minute from the same IP → `429 Too Many Requests`
1176
+
1177
+ > Always combine Basic Auth with `ssl` in production — credentials are transmitted in plain text otherwise.
1178
+
1179
+ ---
1180
+
1181
+ ### URL migration (permanent redirects)
1182
+
1183
+ Redirect old URLs to new ones after a site restructure, without breaking existing links or SEO rankings.
1184
+
1185
+ ```json
1186
+ {
1187
+ "port": 8080,
1188
+ "redirects": [
1189
+ { "from": "/about.html", "to": "/about", "status": 301 },
1190
+ { "from": "/products.html", "to": "/products", "status": 301 },
1191
+ { "from": "/blog/:slug", "to": "/posts/:slug", "status": 301 }
1192
+ ],
1193
+ "folders": "./public"
1194
+ }
1195
+ ```
1196
+
1197
+ Or as an object map for simple path-to-path redirects:
1198
+
1199
+ ```json
1200
+ {
1201
+ "redirects": {
1202
+ "/old-home": "/",
1203
+ "/old-about": "/about",
1204
+ "/legacy-api": "https://api.example.com"
1205
+ }
1206
+ }
1207
+ ```
1208
+
1209
+ ---
1210
+
1211
+ ### Load-balanced API proxy
1212
+
1213
+ Distribute API traffic across multiple back-end instances using round-robin load balancing. No external load balancer required.
1214
+
1215
+ ```json
1216
+ {
1217
+ "port": 8080,
1218
+ "folders": "./public",
1219
+ "proxy": {
1220
+ "/api": [
1221
+ "http://backend-1:3000",
1222
+ "http://backend-2:3000",
1223
+ "http://backend-3:3000"
1224
+ ]
1225
+ }
1226
+ }
1227
+ ```
1228
+
1229
+ Requests to `/api/*` are forwarded to the three back-ends in turn. If a back-end is down, its slot in the rotation still receives requests — add a health check at the application level or use a dedicated load balancer for automatic failover.
1230
+
1231
+ ---
1232
+
1233
+ ### File upload server
1234
+
1235
+ Accept file uploads from a web form or API client and serve them back over HTTP.
1236
+
1237
+ ```json
1238
+ {
1239
+ "port": 8080,
1240
+ "upload": [
1241
+ {
1242
+ "path": "/photos",
1243
+ "dir": "./storage/photos",
1244
+ "maxFileSize": 5242880,
1245
+ "allowedTypes": ["image/jpeg", "image/png", "image/webp"]
1246
+ },
1247
+ {
1248
+ "path": "/documents",
1249
+ "dir": "./storage/docs",
1250
+ "maxFileSize": 10485760,
1251
+ "allowedTypes": ["application/pdf"]
1252
+ }
1253
+ ]
1254
+ }
1255
+ ```
1256
+
1257
+ Upload a photo:
1258
+
1259
+ ```shell
1260
+ curl -F "file=@photo.jpg" http://localhost:8080/photos
1261
+ # {"files":[{"file":"photo-1700000000000-123456789.jpg","size":45678,"originalName":"photo.jpg"}]}
1262
+ ```
1263
+
1264
+ Retrieve it:
1265
+
1266
+ ```shell
1267
+ curl http://localhost:8080/photos/photo-1700000000000-123456789.jpg
1268
+ ```
1269
+
1270
+ ---
1271
+
1272
+ ### Health check for Docker / Kubernetes
1273
+
1274
+ Add a health check endpoint and write access logs to a file — a common pattern for containerized deployments.
1275
+
1276
+ ```json
1277
+ {
1278
+ "port": 8080,
1279
+ "healthCheck": { "path": "/health" },
1280
+ "logging": { "format": "combined", "file": "/var/log/app/access.log" },
1281
+ "compression": true,
1282
+ "folders": "./public",
1283
+ "proxy": {
1284
+ "/api": "http://backend:3000"
1285
+ }
1286
+ }
1287
+ ```
1288
+
1289
+ Docker `HEALTHCHECK`:
1290
+
1291
+ ```dockerfile
1292
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
1293
+ CMD curl -f http://localhost:8080/health || exit 1
1294
+ ```
1295
+
1296
+ Kubernetes liveness/readiness probe:
1297
+
1298
+ ```yaml
1299
+ livenessProbe:
1300
+ httpGet:
1301
+ path: /health
1302
+ port: 8080
1303
+ initialDelaySeconds: 10
1304
+ periodSeconds: 30
1305
+ ```
1306
+
1307
+ ---
1308
+
533
1309
  ### CORS headers + rich error responses
534
1310
 
535
1311
  ```json
@@ -807,6 +1583,15 @@ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
807
1583
  - [Express](https://expressjs.com/) — HTTP server framework
808
1584
  - [express-http-proxy](https://github.com/villadora/express-http-proxy) — reverse proxy middleware
809
1585
  - [Morgan](https://github.com/expressjs/morgan) — HTTP request logger
1586
+ - [compression](https://github.com/expressjs/compression) — gzip/deflate response compression
1587
+ - [helmet](https://helmetjs.github.io/) — security HTTP headers
1588
+ - [cors](https://github.com/expressjs/cors) — CORS headers and preflight handling
1589
+ - [serve-favicon](https://github.com/expressjs/serve-favicon) — efficient favicon serving
1590
+ - [response-time](https://github.com/expressjs/response-time) — X-Response-Time header
1591
+ - [express-rate-limit](https://express-rate-limit.mintlify.app/) — request rate limiting
1592
+ - [express-basic-auth](https://github.com/LionC/express-basic-auth) — HTTP Basic Authentication
1593
+ - CGI support — built on Node.js `child_process.spawn` (no external dependency)
1594
+ - [multer](https://github.com/expressjs/multer) — multipart/form-data file upload handling
810
1595
  - [PM2](https://pm2.keymetrics.io/) — production process manager with clustering
811
1596
  - [Biome](https://biomejs.dev/) — fast linter and formatter (Rust-based)
812
1597
  - [Cypress](https://www.cypress.io/) — E2E testing framework