@lopatnov/express-reverse-proxy 4.0.0 → 5.0.1
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/LICENSE +192 -192
- package/README.md +658 -17
- package/package.json +14 -9
- package/server-config.schema.json +321 -0
- package/server.js +255 -10
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
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)
|
|
23
27
|
- [logging](#logging)
|
|
@@ -27,13 +31,31 @@
|
|
|
27
31
|
- [cors](#cors)
|
|
28
32
|
- [favicon](#favicon)
|
|
29
33
|
- [responseTime](#responsetime)
|
|
34
|
+
- [rateLimit](#ratelimit)
|
|
35
|
+
- [basicAuth](#basicauth)
|
|
36
|
+
- [healthCheck](#healthcheck)
|
|
37
|
+
- [cgi](#cgi)
|
|
38
|
+
- [upload](#upload)
|
|
30
39
|
- [headers](#headers)
|
|
40
|
+
- [redirects](#redirects)
|
|
31
41
|
- [folders](#folders)
|
|
32
42
|
- [proxy](#proxy)
|
|
33
43
|
- [unhandled](#unhandled)
|
|
34
44
|
- [host](#host)
|
|
35
45
|
- [ssl](#ssl)
|
|
36
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)
|
|
37
59
|
- [Docker & PM2](#docker--pm2)
|
|
38
60
|
- [Testing](#testing)
|
|
39
61
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -67,7 +89,13 @@ npx @lopatnov/express-reverse-proxy
|
|
|
67
89
|
|
|
68
90
|
## Quick Start
|
|
69
91
|
|
|
70
|
-
1.
|
|
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:
|
|
71
99
|
|
|
72
100
|
```json
|
|
73
101
|
{
|
|
@@ -139,24 +167,48 @@ Browser
|
|
|
139
167
|
|
|
140
168
|
## How It Works
|
|
141
169
|
|
|
142
|
-
Every incoming request
|
|
170
|
+
Every incoming request passes through the middleware chain in this order:
|
|
143
171
|
|
|
144
172
|
```
|
|
145
173
|
Request
|
|
146
174
|
│
|
|
147
|
-
├─▶
|
|
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
|
|
148
192
|
│
|
|
149
193
|
├─▶ Static files checked (folders, in order)
|
|
150
194
|
│ └─▶ File found → serve it
|
|
151
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
|
+
│
|
|
152
203
|
├─▶ Reverse proxy rules checked (proxy)
|
|
153
204
|
│ └─▶ Path matches → forward to back-end → return response
|
|
205
|
+
│ (round-robin if multiple targets configured)
|
|
154
206
|
│
|
|
155
207
|
└─▶ No match → unhandled handler (by Accept header)
|
|
156
|
-
└─▶ Return status + body
|
|
208
|
+
└─▶ Return status + body
|
|
157
209
|
```
|
|
158
210
|
|
|
159
|
-
|
|
211
|
+
Options that are not configured are skipped entirely. Static files always take priority over proxy rules.
|
|
160
212
|
|
|
161
213
|
---
|
|
162
214
|
|
|
@@ -175,14 +227,51 @@ lerp [options]
|
|
|
175
227
|
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
176
228
|
| `--help` | Print help and exit |
|
|
177
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 |
|
|
178
231
|
| `--cluster [action]` | Manage the PM2 cluster. Action defaults to `start` when omitted |
|
|
179
232
|
| `--cluster-config <file>` | Path to a custom PM2 ecosystem config file. Default: `ecosystem.config.cjs` next to `server.js` |
|
|
180
233
|
|
|
181
|
-
### --
|
|
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.
|
|
182
271
|
|
|
183
272
|
| Action | Description |
|
|
184
273
|
| --------- | ------------------------------------------------------ |
|
|
185
|
-
| `start` | Start the
|
|
274
|
+
| `start` | Start the cluster (default when action is omitted) |
|
|
186
275
|
| `stop` | Stop all cluster instances |
|
|
187
276
|
| `restart` | Restart all cluster instances |
|
|
188
277
|
| `status` | Show PM2 process status table |
|
|
@@ -190,7 +279,7 @@ lerp [options]
|
|
|
190
279
|
| `monitor` | Open the PM2 real-time monitor |
|
|
191
280
|
|
|
192
281
|
```shell
|
|
193
|
-
express-reverse-proxy --cluster
|
|
282
|
+
express-reverse-proxy --cluster # same as --cluster start
|
|
194
283
|
express-reverse-proxy --cluster start
|
|
195
284
|
express-reverse-proxy --cluster stop
|
|
196
285
|
express-reverse-proxy --cluster restart
|
|
@@ -199,25 +288,41 @@ express-reverse-proxy --cluster logs
|
|
|
199
288
|
express-reverse-proxy --cluster monitor
|
|
200
289
|
```
|
|
201
290
|
|
|
202
|
-
Pass a custom config to cluster workers
|
|
291
|
+
Pass `--config` to forward a custom config path to all cluster workers:
|
|
203
292
|
|
|
204
293
|
```shell
|
|
205
294
|
express-reverse-proxy --cluster start --config ./configs/prod.json
|
|
206
295
|
```
|
|
207
296
|
|
|
208
|
-
|
|
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`.
|
|
209
300
|
|
|
210
301
|
```shell
|
|
211
302
|
express-reverse-proxy --cluster start --cluster-config ./my-ecosystem.config.cjs
|
|
212
303
|
express-reverse-proxy --cluster restart --cluster-config /etc/myapp/ecosystem.config.cjs
|
|
213
304
|
```
|
|
214
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
|
+
|
|
215
308
|
---
|
|
216
309
|
|
|
217
310
|
## Configuration
|
|
218
311
|
|
|
219
312
|
All configuration lives in a single JSON file (default `server-config.json`).
|
|
220
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
|
+
|
|
221
326
|
### Environment variables
|
|
222
327
|
|
|
223
328
|
| Variable | Default | Description |
|
|
@@ -237,7 +342,7 @@ The port the server listens on. Defaults to `8000`. Can also be set via the `POR
|
|
|
237
342
|
|
|
238
343
|
### logging
|
|
239
344
|
|
|
240
|
-
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.
|
|
241
346
|
|
|
242
347
|
```json
|
|
243
348
|
{
|
|
@@ -247,6 +352,23 @@ Controls HTTP request logging (Morgan). Enabled by default. Set to `false` to si
|
|
|
247
352
|
}
|
|
248
353
|
```
|
|
249
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
|
+
|
|
250
372
|
### hotReload
|
|
251
373
|
|
|
252
374
|
Watches the `folders` directories for file changes and automatically reloads connected browser tabs. Uses Server-Sent Events (SSE). Intended for local development only.
|
|
@@ -296,6 +418,42 @@ Add headers to every response — useful for CORS in development.
|
|
|
296
418
|
}
|
|
297
419
|
```
|
|
298
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
|
+
|
|
299
457
|
### folders
|
|
300
458
|
|
|
301
459
|
Serve static files. Supports three forms:
|
|
@@ -375,6 +533,18 @@ Forward requests to a back-end server. Supports three forms:
|
|
|
375
533
|
}
|
|
376
534
|
```
|
|
377
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
|
+
|
|
378
548
|
### unhandled
|
|
379
549
|
|
|
380
550
|
Control responses when no static file or proxy rule matches. Rules are selected by the request's `Accept` header.
|
|
@@ -455,11 +625,12 @@ To use multi-site mode, make the config file an **array** instead of an object.
|
|
|
455
625
|
|
|
456
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.
|
|
457
627
|
|
|
458
|
-
| Field
|
|
459
|
-
|
|
|
460
|
-
| `key`
|
|
461
|
-
| `cert`
|
|
462
|
-
| `ca`
|
|
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 |
|
|
463
634
|
|
|
464
635
|
Paths are resolved **relative to the config file**, not the current working directory.
|
|
465
636
|
|
|
@@ -477,6 +648,22 @@ Paths are resolved **relative to the config file**, not the current working dire
|
|
|
477
648
|
}
|
|
478
649
|
```
|
|
479
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
|
+
|
|
480
667
|
> All site configs on the same port must either all have `ssl` or none — mixing is a startup error.
|
|
481
668
|
|
|
482
669
|
### compression
|
|
@@ -581,6 +768,243 @@ With custom precision (see [response-time docs](https://github.com/expressjs/res
|
|
|
581
768
|
}
|
|
582
769
|
```
|
|
583
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
|
+
|
|
584
1008
|
---
|
|
585
1009
|
|
|
586
1010
|
## Configuration Recipes
|
|
@@ -618,6 +1042,65 @@ Only `/api/*` requests go to the back-end; everything else stays local.
|
|
|
618
1042
|
- `GET /api/users` → proxied to `http://localhost:4000/users`
|
|
619
1043
|
- `GET /missing` → 404 Not Found
|
|
620
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
|
+
|
|
621
1104
|
### HTTPS with a self-signed certificate (local dev)
|
|
622
1105
|
|
|
623
1106
|
```shell
|
|
@@ -643,7 +1126,7 @@ openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
|
|
|
643
1126
|
Start and open in browser (accept the self-signed cert warning):
|
|
644
1127
|
|
|
645
1128
|
```shell
|
|
646
|
-
|
|
1129
|
+
express-reverse-proxy --config server-config.json
|
|
647
1130
|
# [listen] https://localhost:8443
|
|
648
1131
|
```
|
|
649
1132
|
|
|
@@ -669,6 +1152,160 @@ Enable security headers, CORS, and response compression in one config:
|
|
|
669
1152
|
|
|
670
1153
|
---
|
|
671
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
|
+
|
|
672
1309
|
### CORS headers + rich error responses
|
|
673
1310
|
|
|
674
1311
|
```json
|
|
@@ -951,6 +1588,10 @@ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
|
|
|
951
1588
|
- [cors](https://github.com/expressjs/cors) — CORS headers and preflight handling
|
|
952
1589
|
- [serve-favicon](https://github.com/expressjs/serve-favicon) — efficient favicon serving
|
|
953
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
|
|
954
1595
|
- [PM2](https://pm2.keymetrics.io/) — production process manager with clustering
|
|
955
1596
|
- [Biome](https://biomejs.dev/) — fast linter and formatter (Rust-based)
|
|
956
1597
|
- [Cypress](https://www.cypress.io/) — E2E testing framework
|