@derian-cordoba/api-gateway 1.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.
Files changed (29) hide show
  1. package/.env.example +32 -0
  2. package/README.md +382 -0
  3. package/dist/src/apps/api-gateway/App.d.ts +12 -0
  4. package/dist/src/apps/api-gateway/App.js +35 -0
  5. package/dist/src/apps/api-gateway/Server.d.ts +27 -0
  6. package/dist/src/apps/api-gateway/Server.js +79 -0
  7. package/dist/src/apps/api-gateway/config/app-env.d.ts +11 -0
  8. package/dist/src/apps/api-gateway/config/app-env.js +13 -0
  9. package/dist/src/apps/api-gateway/config/cors/config.d.ts +6 -0
  10. package/dist/src/apps/api-gateway/config/cors/config.js +13 -0
  11. package/dist/src/apps/api-gateway/config/env/config.d.ts +4 -0
  12. package/dist/src/apps/api-gateway/config/env/config.js +6 -0
  13. package/dist/src/apps/api-gateway/config/gateway/config.d.ts +9 -0
  14. package/dist/src/apps/api-gateway/config/gateway/config.js +9 -0
  15. package/dist/src/apps/api-gateway/config/routes/config.d.ts +4 -0
  16. package/dist/src/apps/api-gateway/config/routes/config.js +6 -0
  17. package/dist/src/apps/api-gateway/index.d.ts +2 -0
  18. package/dist/src/apps/api-gateway/index.js +47 -0
  19. package/dist/src/apps/api-gateway/logger.d.ts +2 -0
  20. package/dist/src/apps/api-gateway/logger.js +18 -0
  21. package/dist/src/apps/api-gateway/routes/HealthRouter.d.ts +2 -0
  22. package/dist/src/apps/api-gateway/routes/HealthRouter.js +18 -0
  23. package/dist/src/apps/api-gateway/routes/ProxyManager.d.ts +13 -0
  24. package/dist/src/apps/api-gateway/routes/ProxyManager.js +97 -0
  25. package/dist/src/apps/api-gateway/routes/RouteValidator.d.ts +21 -0
  26. package/dist/src/apps/api-gateway/routes/RouteValidator.js +36 -0
  27. package/dist/src/apps/api-gateway/routes/Router.d.ts +18 -0
  28. package/dist/src/apps/api-gateway/routes/Router.js +120 -0
  29. package/package.json +70 -0
package/.env.example ADDED
@@ -0,0 +1,32 @@
1
+ # ── Server ───────────────────────────────────────────────────────────────────
2
+ # Port the gateway listens on. GATEWAY_PORT takes priority over PORT.
3
+ GATEWAY_PORT=3000
4
+
5
+ # Optional URL prefix mounted before all gateway routes.
6
+ # Example: GATEWAY_PREFIX=/api/v1 → routes become /api/v1/<baseURL>
7
+ GATEWAY_PREFIX=
8
+
9
+ # ── Logging ──────────────────────────────────────────────────────────────────
10
+ # Log level: trace | debug | info | warn | error | fatal
11
+ LOG_LEVEL=info
12
+
13
+ # Set to "production" to disable pino-pretty and emit newline-delimited JSON.
14
+ NODE_ENV=development
15
+
16
+ # ── CORS ─────────────────────────────────────────────────────────────────────
17
+ # Comma-separated list of allowed origins. Use * to allow all origins.
18
+ CORS_ORIGINS=*
19
+
20
+ # Comma-separated list of allowed HTTP methods.
21
+ CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
22
+
23
+ # Comma-separated list of allowed request headers.
24
+ CORS_HEADERS=Content-Type,Authorization
25
+
26
+ # ── Routes ───────────────────────────────────────────────────────────────────
27
+ # Path to the JSON file that defines proxy routes (relative to process cwd).
28
+ ROUTES_FILE_PATH=routes.json
29
+
30
+ # Inline route definitions as a JSON array (merged with ROUTES_FILE_PATH).
31
+ # Example: ROUTES=[{"baseURL":"/svc","proxy":{"target":"http://localhost:4000","changeOrigin":true}}]
32
+ ROUTES=
package/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # API Gateway
2
+
3
+ A generic, configuration-driven HTTP API gateway. Routes incoming requests to upstream services via a JSON config file or environment variable, with per-route rate limiting, request validation, structured logging, and full security headers out of the box.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Requirements](#requirements)
11
+ - [Getting Started](#getting-started)
12
+ - [Configuration](#configuration)
13
+ - [Environment Variables](#environment-variables)
14
+ - [Route Configuration](#route-configuration)
15
+ - [Running the Gateway](#running-the-gateway)
16
+ - [Health Check](#health-check)
17
+ - [Project Structure](#project-structure)
18
+ - [Example Project](#example-project)
19
+ - [Architecture](#architecture)
20
+
21
+ ---
22
+
23
+ ## Features
24
+
25
+ - **Configuration-driven routing** — define proxy routes in a JSON file, an environment variable, or both; changes take effect on restart with zero code changes
26
+ - **Per-route rate limiting** — each route can declare its own `max` requests / `windowMs` window, enforced by `express-rate-limit`
27
+ - **Startup validation** — route config is validated with Zod at boot time; the process exits with a descriptive error rather than silently misbehaving
28
+ - **Structured logging** — `pino` + `pino-http` emit newline-delimited JSON in production and human-readable output (via `pino-pretty`) in development
29
+ - **Security headers** — full `helmet` defaults applied to every response (`CSP`, `HSTS`, `X-Frame-Options`, `X-Content-Type-Options`, etc.)
30
+ - **Configurable CORS** — origins, methods, and allowed headers controlled via environment variables
31
+ - **Health check endpoint** — `GET /health` returns uptime, version, and timestamp; always available regardless of configured routes
32
+ - **Optional URL prefix** — mount all routes under a shared prefix (e.g. `/api/v1`) via `GATEWAY_PREFIX`
33
+ - **Body forwarding** — JSON bodies on `POST`, `PUT`, and `PATCH` requests are correctly forwarded to upstreams (`fixRequestBody`)
34
+ - **Graceful shutdown** — `SIGINT` and `uncaughtException` handlers stop the server cleanly before exiting
35
+
36
+ ---
37
+
38
+ ## Requirements
39
+
40
+ - Node.js 18+
41
+ - pnpm 10+
42
+
43
+ ---
44
+
45
+ ## Getting Started
46
+
47
+ ```bash
48
+ # 1. Clone and install
49
+ git clone <repo-url>
50
+ cd api-gateway
51
+ pnpm install
52
+
53
+ # 2. Create your env file
54
+ cp .env.example .env
55
+
56
+ # 3. Create a routes config (see Route Configuration below)
57
+ cp examples/basic/routes.json routes.json # or write your own
58
+
59
+ # 4. Start in development mode (hot-reload)
60
+ pnpm dev
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Configuration
66
+
67
+ ### Environment Variables
68
+
69
+ Copy `.env.example` to `.env` and edit as needed.
70
+
71
+ #### Server
72
+
73
+ | Variable | Default | Description |
74
+ |---|---|---|
75
+ | `GATEWAY_PORT` | `3000` | Port the gateway listens on. Takes priority over `PORT`. |
76
+ | `PORT` | `3000` | Fallback port when `GATEWAY_PORT` is not set. |
77
+ | `GATEWAY_PREFIX` | _(none)_ | Optional path prefix for all routes. Example: `/api/v1` makes proxy routes reachable at `/api/v1/<baseURL>` and the health check at `/api/v1/health`. |
78
+
79
+ #### Logging
80
+
81
+ | Variable | Default | Description |
82
+ |---|---|---|
83
+ | `NODE_ENV` | `development` | Set to `production` to disable `pino-pretty` and emit newline-delimited JSON. |
84
+ | `LOG_LEVEL` | `info` | Pino log level: `trace` · `debug` · `info` · `warn` · `error` · `fatal`. |
85
+
86
+ #### CORS
87
+
88
+ | Variable | Default | Description |
89
+ |---|---|---|
90
+ | `CORS_ORIGINS` | `*` | Comma-separated list of allowed origins. Use `*` to allow all. |
91
+ | `CORS_METHODS` | `GET,POST,PUT,DELETE,PATCH,OPTIONS` | Comma-separated list of allowed HTTP methods. |
92
+ | `CORS_HEADERS` | `Content-Type,Authorization` | Comma-separated list of allowed request headers. |
93
+
94
+ #### Routes
95
+
96
+ | Variable | Default | Description |
97
+ |---|---|---|
98
+ | `ROUTES_FILE_PATH` | `routes.json` | Path to the JSON route config file, relative to `process.cwd()`. |
99
+ | `ROUTES` | _(none)_ | Inline route definitions as a JSON array. Merged with `ROUTES_FILE_PATH`. Useful for containerised deployments where injecting a file is inconvenient. |
100
+
101
+ ---
102
+
103
+ ### Route Configuration
104
+
105
+ Routes are defined as a JSON array. Each entry is a **Gateway** object:
106
+
107
+ ```ts
108
+ {
109
+ baseURL: string // required — path prefix to match, must start with "/"
110
+ proxy: Proxy // required — upstream proxy settings
111
+ rateLimit?: RateLimit // optional — per-route rate limiting
112
+ }
113
+ ```
114
+
115
+ #### `Proxy`
116
+
117
+ | Field | Type | Required | Description |
118
+ |---|---|---|---|
119
+ | `target` | `string` | ✅ | Target upstream URL (must be a valid URL). |
120
+ | `changeOrigin` | `boolean` | — | Rewrite the `Host` header to the target origin. |
121
+ | `pathRewrite` | `{ [pattern]: replacement }` | — | Regex path rewrite rules applied before forwarding. |
122
+ | `headers` | `{ [name]: value }` | — | Extra headers added to every forwarded request. |
123
+ | `isSecure` | `boolean` | — | Verify the upstream TLS certificate. |
124
+ | `method` | `string` | — | Override the HTTP method forwarded to the upstream. |
125
+ | `timeout` | `number` | — | Proxy request timeout in milliseconds. |
126
+
127
+ #### `RateLimit`
128
+
129
+ | Field | Type | Required | Description |
130
+ |---|---|---|---|
131
+ | `max` | `number` | ✅ | Maximum number of requests allowed per window. |
132
+ | `windowMs` | `number` | ✅ | Time window in milliseconds. |
133
+ | `statusCode` | `number` | — | HTTP status returned when the limit is exceeded (default: `429`). |
134
+ | `message` | `string` | — | Response message when the limit is exceeded (default: `"Too many requests"`). |
135
+
136
+ Responses include standard `RateLimit-*` headers (RFC draft-8).
137
+
138
+ #### Example `routes.json`
139
+
140
+ ```json
141
+ [
142
+ {
143
+ "baseURL": "/users",
144
+ "proxy": {
145
+ "target": "http://users-service:3001",
146
+ "changeOrigin": true,
147
+ "pathRewrite": { "^/users": "" }
148
+ },
149
+ "rateLimit": {
150
+ "max": 100,
151
+ "windowMs": 60000,
152
+ "statusCode": 429,
153
+ "message": "Too many requests. Please try again in a minute."
154
+ }
155
+ },
156
+ {
157
+ "baseURL": "/orders",
158
+ "proxy": {
159
+ "target": "http://orders-service:3002",
160
+ "changeOrigin": true,
161
+ "pathRewrite": { "^/orders": "" },
162
+ "headers": {
163
+ "X-Internal-Source": "api-gateway"
164
+ },
165
+ "timeout": 5000
166
+ }
167
+ }
168
+ ]
169
+ ```
170
+
171
+ Config is validated with [Zod](https://zod.dev) at startup. If any route is invalid the process exits immediately with a detailed per-field error message.
172
+
173
+ Routes are loaded from two sources and **merged**:
174
+
175
+ 1. `ROUTES_FILE_PATH` — JSON file on disk (missing file is a warning, not an error)
176
+ 2. `ROUTES` — JSON array in an environment variable
177
+
178
+ ---
179
+
180
+ ## Running the Gateway
181
+
182
+ ### Development (hot-reload)
183
+
184
+ ```bash
185
+ pnpm dev
186
+ ```
187
+
188
+ Uses `ts-node-dev` to transpile on the fly and restart on file changes. Logs are pretty-printed via `pino-pretty`.
189
+
190
+ ### Production
191
+
192
+ ```bash
193
+ # Compile TypeScript
194
+ pnpm build
195
+
196
+ # Run the compiled output
197
+ pnpm start
198
+ ```
199
+
200
+ In production (`NODE_ENV=production`) logs are emitted as newline-delimited JSON suitable for log aggregators (Datadog, Loki, CloudWatch, etc.).
201
+
202
+ ---
203
+
204
+ ## Health Check
205
+
206
+ The gateway exposes a built-in health check endpoint that is always available, independent of the configured proxy routes.
207
+
208
+ ```
209
+ GET /health
210
+ ```
211
+
212
+ If `GATEWAY_PREFIX` is set, the endpoint is available at `<GATEWAY_PREFIX>/health`.
213
+
214
+ **Response `200 OK`:**
215
+
216
+ ```json
217
+ {
218
+ "status": "ok",
219
+ "uptime": 42,
220
+ "version": "1.0.0",
221
+ "timestamp": "2026-06-18T00:00:00.000Z"
222
+ }
223
+ ```
224
+
225
+ | Field | Description |
226
+ |---|---|
227
+ | `status` | Always `"ok"` when the process is alive. |
228
+ | `uptime` | Seconds since the gateway process started. |
229
+ | `version` | Value of `npm_package_version` (set automatically by npm/pnpm). |
230
+ | `timestamp` | ISO 8601 timestamp of the response. |
231
+
232
+ ---
233
+
234
+ ## Project Structure
235
+
236
+ ```
237
+ src/apps/api-gateway/
238
+ ├── index.ts # Entry point — bootstraps the app, registers process signals
239
+ ├── App.ts # Thin lifecycle wrapper (start / stop)
240
+ ├── Server.ts # HTTP server creation and prefix mounting
241
+ ├── logger.ts # Pino logger singleton
242
+
243
+ ├── config/
244
+ │ ├── app-env.ts # Aggregates all config modules into a single AppEnv object
245
+ │ ├── env/config.ts # NODE_ENV → isDev flag
246
+ │ ├── gateway/config.ts # GATEWAY_PORT, GATEWAY_PREFIX
247
+ │ ├── cors/config.ts # CORS_ORIGINS, CORS_METHODS, CORS_HEADERS
248
+ │ └── routes/config.ts # ROUTES_FILE_PATH
249
+
250
+ ├── routes/
251
+ │ ├── Router.ts # Middleware pipeline (logging → security → CORS → body → proxy → errors)
252
+ │ ├── ProxyManager.ts # Reads, validates, and registers proxy routes
253
+ │ ├── RouteValidator.ts # Zod schemas for Gateway / Proxy / RateLimit types
254
+ │ └── HealthRouter.ts # GET /health handler
255
+
256
+ └── types/
257
+ ├── gateway.d.ts # Gateway type
258
+ ├── proxy.d.ts # Proxy type
259
+ └── rate-limit.d.ts # RateLimit type
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Example Project
265
+
266
+ `examples/basic/` contains a self-contained demo with two mock upstream services and a pre-built gateway config.
267
+
268
+ ### What's included
269
+
270
+ | File | Description |
271
+ |---|---|
272
+ | `routes.json` | Gateway config with two routes (`/users`, `/products`) each with rate limiting and path rewriting |
273
+ | `.env` | Gateway environment for the example |
274
+ | `upstream-users.js` | Mock Users service on port `4001` |
275
+ | `upstream-products.js` | Mock Products service on port `4002` |
276
+ | `run.sh` | Starts all three processes and stops them together on Ctrl+C |
277
+
278
+ ### Running the example
279
+
280
+ ```bash
281
+ pnpm example
282
+ ```
283
+
284
+ This copies `examples/basic/.env` to the project root and starts all three services. Once running:
285
+
286
+ | Endpoint | Description |
287
+ |---|---|
288
+ | `http://localhost:3000/health` | Gateway health check |
289
+ | `http://localhost:3000/users` | Proxied to Users service |
290
+ | `http://localhost:3000/products` | Proxied to Products service |
291
+
292
+ ### Users service endpoints (`/users`)
293
+
294
+ | Method | Path | Description |
295
+ |---|---|---|
296
+ | `GET` | `/users` | List all users |
297
+ | `GET` | `/users/:id` | Get a user by ID |
298
+ | `POST` | `/users` | Create a user (body: `{ name, email }`) |
299
+ | `DELETE` | `/users/:id` | Delete a user by ID |
300
+
301
+ ### Products service endpoints (`/products`)
302
+
303
+ | Method | Path | Description |
304
+ |---|---|---|
305
+ | `GET` | `/products` | List all products |
306
+ | `GET` | `/products/:id` | Get a product by ID |
307
+ | `POST` | `/products` | Create a product (body: `{ name, price }`) |
308
+ | `PATCH` | `/products/:id/stock` | Adjust stock (body: `{ quantity: number }`) |
309
+
310
+ ### Example requests
311
+
312
+ ```bash
313
+ # List users
314
+ curl http://localhost:3000/users
315
+
316
+ # Create a user
317
+ curl -X POST http://localhost:3000/users \
318
+ -H "Content-Type: application/json" \
319
+ -d '{"name": "Alice", "email": "alice@example.com"}'
320
+
321
+ # Get a product
322
+ curl http://localhost:3000/products/1
323
+
324
+ # Update product stock
325
+ curl -X PATCH http://localhost:3000/products/1/stock \
326
+ -H "Content-Type: application/json" \
327
+ -d '{"quantity": 25}'
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Architecture
333
+
334
+ ### Request lifecycle
335
+
336
+ ```
337
+ Client
338
+
339
+
340
+ Express app
341
+
342
+ ├─ pino-http structured request/response logging
343
+ ├─ helmet security headers (CSP, HSTS, X-Frame-Options, …)
344
+ ├─ cors configurable origin / method / header policy
345
+ ├─ express.json body parsing
346
+ ├─ compression gzip response compression
347
+
348
+ ├─ GET /health health check — short-circuits here
349
+
350
+ ├─ express-rate-limit per-route request throttling (applied per baseURL)
351
+ ├─ http-proxy-middleware proxies request to upstream, forwards body
352
+
353
+ └─ error handler catches unhandled errors → 500 JSON response
354
+ ```
355
+
356
+ ### Startup sequence
357
+
358
+ ```
359
+ index.ts
360
+
361
+ ├─ dotenv.config() load .env before any module reads process.env
362
+ ├─ new App()
363
+ │ └─ new Server()
364
+ │ ├─ appEnv resolved config modules read from process.env
365
+ │ └─ new Router()
366
+
367
+ └─ app.start()
368
+ ├─ router.init() async: reads routes file, validates, registers middleware
369
+ └─ httpServer.listen() starts accepting connections only after routes are ready
370
+ ```
371
+
372
+ ### Route loading
373
+
374
+ Routes are loaded from two sources at startup and merged into a single array before validation:
375
+
376
+ ```
377
+ ROUTES_FILE_PATH (JSON file) ──┐
378
+ ├──► merge ──► Zod validation ──► register proxy routes
379
+ ROUTES (env var JSON array) ──┘
380
+ ```
381
+
382
+ If either source is missing or contains invalid JSON it is skipped with a warning. If the merged result fails Zod validation, the process exits with a descriptive error.
@@ -0,0 +1,12 @@
1
+ export declare class App {
2
+ private readonly server;
3
+ constructor();
4
+ /**
5
+ * Start the HTTP server
6
+ */
7
+ start(): Promise<void>;
8
+ /**
9
+ * Configure security headers using Helmet
10
+ */
11
+ stop(): Promise<void>;
12
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.App = void 0;
13
+ const Server_1 = require("./Server");
14
+ class App {
15
+ constructor() {
16
+ this.server = new Server_1.Server();
17
+ }
18
+ /**
19
+ * Start the HTTP server
20
+ */
21
+ start() {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ return yield this.server.start();
24
+ });
25
+ }
26
+ /**
27
+ * Configure security headers using Helmet
28
+ */
29
+ stop() {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ return yield this.server.stop();
32
+ });
33
+ }
34
+ }
35
+ exports.App = App;
@@ -0,0 +1,27 @@
1
+ import { type Express } from "express";
2
+ export declare class Server {
3
+ private readonly app;
4
+ private readonly router;
5
+ private readonly httpServer;
6
+ private readonly port;
7
+ private readonly prefix;
8
+ constructor();
9
+ /**
10
+ * Register all middleware and proxy routes without opening a port.
11
+ * Call this before start() or use it directly in tests with getApp().
12
+ */
13
+ init(): Promise<void>;
14
+ /**
15
+ * Returns the underlying Express application.
16
+ * Useful for integration tests via supertest without binding to a port.
17
+ */
18
+ getApp(): Express;
19
+ /**
20
+ * Initialise routes then start the HTTP server.
21
+ */
22
+ start(): Promise<void>;
23
+ /**
24
+ * Stop the HTTP server gracefully
25
+ */
26
+ stop(): Promise<void>;
27
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Server = void 0;
16
+ const express_1 = __importDefault(require("express"));
17
+ const http_1 = require("http");
18
+ const Router_1 = require("./routes/Router");
19
+ const app_env_1 = require("./config/app-env");
20
+ const logger_1 = require("./logger");
21
+ class Server {
22
+ constructor() {
23
+ this.port = app_env_1.appEnv.gateway.port;
24
+ this.prefix = app_env_1.appEnv.gateway.prefix;
25
+ this.router = new Router_1.Router();
26
+ this.app = (0, express_1.default)();
27
+ this.httpServer = (0, http_1.createServer)(this.app);
28
+ }
29
+ /**
30
+ * Register all middleware and proxy routes without opening a port.
31
+ * Call this before start() or use it directly in tests with getApp().
32
+ */
33
+ init() {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ yield this.router.init();
36
+ this.app.use(this.prefix, this.router.getRouter());
37
+ });
38
+ }
39
+ /**
40
+ * Returns the underlying Express application.
41
+ * Useful for integration tests via supertest without binding to a port.
42
+ */
43
+ getApp() {
44
+ return this.app;
45
+ }
46
+ /**
47
+ * Initialise routes then start the HTTP server.
48
+ */
49
+ start() {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ yield this.init();
52
+ return new Promise((resolve) => {
53
+ this.httpServer.listen(this.port, () => {
54
+ logger_1.logger.info(`Gateway started on port ${this.port} (prefix: ${this.prefix})`);
55
+ resolve();
56
+ });
57
+ });
58
+ });
59
+ }
60
+ /**
61
+ * Stop the HTTP server gracefully
62
+ */
63
+ stop() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ return new Promise((resolve) => {
66
+ this.httpServer.close((error) => {
67
+ if (error) {
68
+ logger_1.logger.warn({ err: error }, "Error while stopping server");
69
+ }
70
+ else {
71
+ logger_1.logger.info("Gateway stopped");
72
+ }
73
+ resolve();
74
+ });
75
+ });
76
+ });
77
+ }
78
+ }
79
+ exports.Server = Server;
@@ -0,0 +1,11 @@
1
+ import { type GatewayConfig } from "./gateway/config";
2
+ import { type CorsConfig } from "./cors/config";
3
+ import { type RoutesConfig } from "./routes/config";
4
+ import { type EnvConfig } from "./env/config";
5
+ export type AppEnv = {
6
+ env: EnvConfig;
7
+ gateway: GatewayConfig;
8
+ cors: CorsConfig;
9
+ routes: RoutesConfig;
10
+ };
11
+ export declare const appEnv: AppEnv;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appEnv = void 0;
4
+ const config_1 = require("./gateway/config");
5
+ const config_2 = require("./cors/config");
6
+ const config_3 = require("./routes/config");
7
+ const config_4 = require("./env/config");
8
+ exports.appEnv = {
9
+ env: config_4.envConfig,
10
+ gateway: config_1.gatewayConfig,
11
+ cors: config_2.corsConfig,
12
+ routes: config_3.routesConfig,
13
+ };
@@ -0,0 +1,6 @@
1
+ export type CorsConfig = {
2
+ origins: string | string[];
3
+ methods: string[];
4
+ allowedHeaders: string[];
5
+ };
6
+ export declare const corsConfig: CorsConfig;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.corsConfig = void 0;
4
+ const { CORS_ORIGINS, CORS_METHODS, CORS_HEADERS } = process.env;
5
+ exports.corsConfig = {
6
+ origins: (CORS_ORIGINS === null || CORS_ORIGINS === void 0 ? void 0 : CORS_ORIGINS.split(",").map((origin) => origin.trim())) || "*",
7
+ methods: CORS_METHODS
8
+ ? CORS_METHODS.split(",").map((method) => method.trim())
9
+ : ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
10
+ allowedHeaders: CORS_HEADERS
11
+ ? CORS_HEADERS.split(",").map((header) => header.trim())
12
+ : ["Content-Type", "Authorization"],
13
+ };
@@ -0,0 +1,4 @@
1
+ export type EnvConfig = {
2
+ isDev: boolean;
3
+ };
4
+ export declare const envConfig: EnvConfig;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envConfig = void 0;
4
+ exports.envConfig = {
5
+ isDev: process.env.NODE_ENV !== "production",
6
+ };
@@ -0,0 +1,9 @@
1
+ export type GatewayConfig = {
2
+ prefix: string;
3
+ port: number;
4
+ };
5
+ export declare const DEFAULT_PORT: number;
6
+ export declare const gatewayConfig: {
7
+ readonly prefix: string;
8
+ readonly port: number;
9
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gatewayConfig = exports.DEFAULT_PORT = void 0;
4
+ const { GATEWAY_PREFIX, GATEWAY_PORT, PORT } = process.env;
5
+ exports.DEFAULT_PORT = 3000;
6
+ exports.gatewayConfig = {
7
+ prefix: GATEWAY_PREFIX || '/',
8
+ port: Number(GATEWAY_PORT || PORT) || exports.DEFAULT_PORT,
9
+ };
@@ -0,0 +1,4 @@
1
+ export type RoutesConfig = {
2
+ filePath: string;
3
+ };
4
+ export declare const routesConfig: RoutesConfig;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routesConfig = void 0;
4
+ exports.routesConfig = {
5
+ filePath: process.env.ROUTES_FILE_PATH || "routes.json",
6
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const dotenv_1 = require("dotenv");
14
+ (0, dotenv_1.config)();
15
+ const App_1 = require("./App");
16
+ const logger_1 = require("./logger");
17
+ function handleError(error) {
18
+ logger_1.logger.error({ err: error }, "Fatal startup error");
19
+ process.exit(1);
20
+ }
21
+ /**
22
+ * Bootstrap the application.
23
+ *
24
+ * This function creates a new instance of the App class and starts it.
25
+ *
26
+ * @returns {void}
27
+ */
28
+ function bootstrap() {
29
+ const app = new App_1.App();
30
+ app.start().catch(handleError);
31
+ // Handle process termination signals
32
+ process.on("SIGINT", () => __awaiter(this, void 0, void 0, function* () {
33
+ yield app.stop();
34
+ process.exit(0);
35
+ }));
36
+ process.on("uncaughtException", (error) => __awaiter(this, void 0, void 0, function* () {
37
+ logger_1.logger.error({ err: error }, "uncaughtException");
38
+ try {
39
+ yield app.stop();
40
+ }
41
+ catch (_a) {
42
+ // ignore stop errors during crash shutdown
43
+ }
44
+ process.exit(1);
45
+ }));
46
+ }
47
+ bootstrap();
@@ -0,0 +1,2 @@
1
+ import pino from "pino";
2
+ export declare const logger: pino.Logger<never, boolean>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = void 0;
7
+ const pino_1 = __importDefault(require("pino"));
8
+ const app_env_1 = require("./config/app-env");
9
+ exports.logger = (0, pino_1.default)(Object.assign({ level: process.env.LOG_LEVEL || "info" }, (app_env_1.appEnv.env.isDev && {
10
+ transport: {
11
+ target: "pino-pretty",
12
+ options: {
13
+ colorize: true,
14
+ ignore: "pid,hostname",
15
+ translateTime: "SYS:HH:MM:ss",
16
+ },
17
+ },
18
+ })));
@@ -0,0 +1,2 @@
1
+ import { Router as ExpressRouter } from "express";
2
+ export declare function createHealthRouter(): ExpressRouter;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createHealthRouter = createHealthRouter;
4
+ const express_1 = require("express");
5
+ const http_status_codes_1 = require("http-status-codes");
6
+ const startTime = Date.now();
7
+ function createHealthRouter() {
8
+ const router = (0, express_1.Router)();
9
+ router.get("/health", (_req, res) => {
10
+ res.status(http_status_codes_1.StatusCodes.OK).json({
11
+ status: "ok",
12
+ uptime: Math.floor((Date.now() - startTime) / 1000),
13
+ version: process.env.npm_package_version || "unknown",
14
+ timestamp: new Date().toISOString(),
15
+ });
16
+ });
17
+ return router;
18
+ }
@@ -0,0 +1,13 @@
1
+ import type { Router } from "express";
2
+ export declare class ProxyManager {
3
+ private readonly router;
4
+ private readonly filePath;
5
+ constructor(router: Router);
6
+ /**
7
+ * Register all proxy routes in the application
8
+ */
9
+ registerProxyRoutes(): Promise<void>;
10
+ private readRoutes;
11
+ private readFileRoutes;
12
+ private readEnvRoutes;
13
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ProxyManager = void 0;
16
+ const http_proxy_middleware_1 = require("http-proxy-middleware");
17
+ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
18
+ const http_status_codes_1 = require("http-status-codes");
19
+ const promises_1 = require("node:fs/promises");
20
+ const RouteValidator_1 = require("./RouteValidator");
21
+ const logger_1 = require("../logger");
22
+ const app_env_1 = require("../config/app-env");
23
+ class ProxyManager {
24
+ constructor(router) {
25
+ this.router = router;
26
+ this.filePath = app_env_1.appEnv.routes.filePath;
27
+ }
28
+ /**
29
+ * Register all proxy routes in the application
30
+ */
31
+ registerProxyRoutes() {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ const routes = yield this.readRoutes();
34
+ if (routes.length === 0) {
35
+ logger_1.logger.warn("No proxy routes configured");
36
+ return;
37
+ }
38
+ routes.forEach((route) => {
39
+ var _a, _b;
40
+ // Apply per-route rate limiting when configured
41
+ if (route.rateLimit) {
42
+ this.router.use(route.baseURL, (0, express_rate_limit_1.default)({
43
+ windowMs: route.rateLimit.windowMs,
44
+ limit: route.rateLimit.max,
45
+ statusCode: (_a = route.rateLimit.statusCode) !== null && _a !== void 0 ? _a : http_status_codes_1.StatusCodes.TOO_MANY_REQUESTS,
46
+ message: (_b = route.rateLimit.message) !== null && _b !== void 0 ? _b : "Too many requests",
47
+ standardHeaders: true,
48
+ legacyHeaders: false,
49
+ }));
50
+ }
51
+ this.router.use(route.baseURL, (0, http_proxy_middleware_1.createProxyMiddleware)(Object.assign(Object.assign({}, route.proxy), { on: {
52
+ // Re-stream the body that express.json() already consumed so the
53
+ // upstream receives the request body correctly on POST/PUT/PATCH.
54
+ proxyReq: http_proxy_middleware_1.fixRequestBody,
55
+ } })));
56
+ logger_1.logger.info({ baseURL: route.baseURL, target: route.proxy.target }, "Registered proxy route");
57
+ });
58
+ });
59
+ }
60
+ readRoutes() {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ const fileRoutes = yield this.readFileRoutes();
63
+ const envRoutes = this.readEnvRoutes();
64
+ const merged = [...fileRoutes, ...envRoutes];
65
+ return (0, RouteValidator_1.validateRoutes)(merged);
66
+ });
67
+ }
68
+ readFileRoutes() {
69
+ return __awaiter(this, void 0, void 0, function* () {
70
+ try {
71
+ const content = yield (0, promises_1.readFile)(this.filePath, "utf-8");
72
+ return JSON.parse(content);
73
+ }
74
+ catch (error) {
75
+ if (error.code === "ENOENT") {
76
+ logger_1.logger.debug({ filePath: this.filePath }, "Routes file not found, skipping");
77
+ return [];
78
+ }
79
+ logger_1.logger.error({ err: error, filePath: this.filePath }, "Failed to read routes file");
80
+ return [];
81
+ }
82
+ });
83
+ }
84
+ readEnvRoutes() {
85
+ const raw = process.env.ROUTES;
86
+ if (!raw)
87
+ return [];
88
+ try {
89
+ return JSON.parse(raw);
90
+ }
91
+ catch (error) {
92
+ logger_1.logger.error({ err: error }, "Failed to parse ROUTES env var as JSON, skipping");
93
+ return [];
94
+ }
95
+ }
96
+ }
97
+ exports.ProxyManager = ProxyManager;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import type { Gateway } from "../types/gateway";
3
+ export declare const GatewaysSchema: z.ZodArray<z.ZodObject<{
4
+ baseURL: z.ZodString;
5
+ proxy: z.ZodObject<{
6
+ target: z.ZodURL;
7
+ isSecure: z.ZodOptional<z.ZodBoolean>;
8
+ changeOrigin: z.ZodOptional<z.ZodBoolean>;
9
+ pathRewrite: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
10
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
+ method: z.ZodOptional<z.ZodString>;
12
+ timeout: z.ZodOptional<z.ZodNumber>;
13
+ }, z.core.$strip>;
14
+ rateLimit: z.ZodOptional<z.ZodObject<{
15
+ max: z.ZodNumber;
16
+ windowMs: z.ZodNumber;
17
+ statusCode: z.ZodOptional<z.ZodNumber>;
18
+ message: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strip>>;
20
+ }, z.core.$strip>>;
21
+ export declare function validateRoutes(data: unknown): Gateway[];
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GatewaysSchema = void 0;
4
+ exports.validateRoutes = validateRoutes;
5
+ const zod_1 = require("zod");
6
+ const ProxySchema = zod_1.z.object({
7
+ target: zod_1.z.url("Proxy target must be a valid URL"),
8
+ isSecure: zod_1.z.boolean().optional(),
9
+ changeOrigin: zod_1.z.boolean().optional(),
10
+ pathRewrite: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
11
+ headers: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
12
+ method: zod_1.z.string().optional(),
13
+ timeout: zod_1.z.number().positive("Proxy timeout must be a positive number").optional(),
14
+ });
15
+ const RateLimitSchema = zod_1.z.object({
16
+ max: zod_1.z.number().positive("Rate limit max must be a positive number"),
17
+ windowMs: zod_1.z.number().positive("Rate limit windowMs must be a positive number"),
18
+ statusCode: zod_1.z.number().optional(),
19
+ message: zod_1.z.string().optional(),
20
+ });
21
+ const GatewaySchema = zod_1.z.object({
22
+ baseURL: zod_1.z.string().startsWith("/", "baseURL must start with /"),
23
+ proxy: ProxySchema,
24
+ rateLimit: RateLimitSchema.optional(),
25
+ });
26
+ exports.GatewaysSchema = zod_1.z.array(GatewaySchema);
27
+ function validateRoutes(data) {
28
+ const result = exports.GatewaysSchema.safeParse(data);
29
+ if (!result.success) {
30
+ const formatted = result.error.issues
31
+ .map((issue) => ` [${issue.path.join(".")}] ${issue.message}`)
32
+ .join("\n");
33
+ throw new Error(`Invalid route configuration:\n${formatted}`);
34
+ }
35
+ return result.data;
36
+ }
@@ -0,0 +1,18 @@
1
+ import { Router as ExpressRouter } from "express";
2
+ export declare class Router {
3
+ private readonly router;
4
+ constructor();
5
+ /**
6
+ * Get the router instance for the application
7
+ */
8
+ getRouter(): ExpressRouter;
9
+ /**
10
+ * Initialise all middleware and routes. Must be awaited before the HTTP
11
+ * server starts listening so that proxy routes are registered in time.
12
+ */
13
+ init(): Promise<void>;
14
+ private configureCors;
15
+ private configureBodyParser;
16
+ private configureProxyManager;
17
+ private configureErrorHandler;
18
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.Router = void 0;
49
+ const express_1 = __importStar(require("express"));
50
+ const http_status_codes_1 = require("http-status-codes");
51
+ const cors_1 = __importDefault(require("cors"));
52
+ const compression_1 = __importDefault(require("compression"));
53
+ const helmet_1 = __importDefault(require("helmet"));
54
+ const pino_http_1 = __importDefault(require("pino-http"));
55
+ const ProxyManager_1 = require("./ProxyManager");
56
+ const HealthRouter_1 = require("./HealthRouter");
57
+ const app_env_1 = require("../config/app-env");
58
+ const logger_1 = require("../logger");
59
+ class Router {
60
+ constructor() {
61
+ this.router = (0, express_1.Router)();
62
+ }
63
+ /**
64
+ * Get the router instance for the application
65
+ */
66
+ getRouter() {
67
+ return this.router;
68
+ }
69
+ /**
70
+ * Initialise all middleware and routes. Must be awaited before the HTTP
71
+ * server starts listening so that proxy routes are registered in time.
72
+ */
73
+ init() {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ // Structured HTTP request logging
76
+ this.router.use((0, pino_http_1.default)({ logger: logger_1.logger }));
77
+ // Security headers (full helmet defaults)
78
+ this.router.use((0, helmet_1.default)());
79
+ // Configurable CORS
80
+ this.configureCors();
81
+ // Body parsing + gzip compression
82
+ this.configureBodyParser();
83
+ // Health check
84
+ this.router.use((0, HealthRouter_1.createHealthRouter)());
85
+ // Proxy routes (async — reads config file / env var)
86
+ yield this.configureProxyManager();
87
+ // Error handler must be registered last
88
+ this.configureErrorHandler();
89
+ });
90
+ }
91
+ configureCors() {
92
+ const { origins, methods, allowedHeaders } = app_env_1.appEnv.cors;
93
+ this.router.use((0, cors_1.default)({
94
+ origin: origins,
95
+ methods,
96
+ allowedHeaders,
97
+ }));
98
+ }
99
+ configureBodyParser() {
100
+ this.router.use(express_1.default.json());
101
+ this.router.use(express_1.default.urlencoded({ extended: true }));
102
+ this.router.use((0, compression_1.default)());
103
+ }
104
+ configureProxyManager() {
105
+ return __awaiter(this, void 0, void 0, function* () {
106
+ const proxyManager = new ProxyManager_1.ProxyManager(this.router);
107
+ yield proxyManager.registerProxyRoutes();
108
+ });
109
+ }
110
+ configureErrorHandler() {
111
+ this.router.use((error, _req, res, _next) => {
112
+ logger_1.logger.error({ err: error }, "Unhandled error");
113
+ res.status(http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR).json({
114
+ error: "Internal Server Error",
115
+ message: error.message,
116
+ });
117
+ });
118
+ }
119
+ }
120
+ exports.Router = Router;
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@derian-cordoba/api-gateway",
3
+ "version": "1.0.0",
4
+ "description": "A generic, configuration-driven HTTP API gateway with per-route rate limiting, structured logging, and security headers",
5
+ "main": "./dist/src/apps/api-gateway/index.js",
6
+ "types": "./dist/src/apps/api-gateway/index.d.ts",
7
+ "bin": {
8
+ "api-gateway": "./dist/src/apps/api-gateway/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ ".env.example",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.0.0",
17
+ "pnpm": ">=10.0.0"
18
+ },
19
+ "keywords": [
20
+ "api-gateway",
21
+ "gateway",
22
+ "proxy",
23
+ "reverse-proxy",
24
+ "express",
25
+ "rate-limiting",
26
+ "typescript",
27
+ "nodejs"
28
+ ],
29
+ "author": "derian-cordoba",
30
+ "license": "MIT",
31
+ "packageManager": "pnpm@10.6.5",
32
+ "scripts": {
33
+ "dev": "NODE_ENV=development ts-node-dev --ignore-watch node_modules --respawn --transpile-only src/apps/api-gateway/index.ts",
34
+ "build": "rm -rf ./dist && tsc -p tsconfig.prod.json",
35
+ "postbuild": "chmod +x dist/src/apps/api-gateway/index.js",
36
+ "start": "node dist/src/apps/api-gateway/index.js",
37
+ "prepare": "pnpm build",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "example": "bash examples/basic/run.sh"
41
+ },
42
+ "devDependencies": {
43
+ "@types/compression": "^1.8.0",
44
+ "@types/cors": "^2.8.18",
45
+ "@types/express": "^5.0.2",
46
+ "@types/node": "^22.15.21",
47
+ "@types/supertest": "^7.2.0",
48
+ "pino-pretty": "^13.1.3",
49
+ "supertest": "^7.2.2",
50
+ "ts-node-dev": "^2.0.0",
51
+ "typescript": "^5.8.3",
52
+ "vitest": "^4.1.9"
53
+ },
54
+ "dependencies": {
55
+ "compression": "^1.8.0",
56
+ "cors": "^2.8.5",
57
+ "dotenv": "^16.5.0",
58
+ "express": "^5.1.0",
59
+ "express-rate-limit": "^8.5.2",
60
+ "helmet": "^8.1.0",
61
+ "http-proxy-middleware": "^3.0.5",
62
+ "http-status-codes": "^2.3.0",
63
+ "pino": "^10.3.1",
64
+ "pino-http": "^11.0.0",
65
+ "zod": "^4.4.3"
66
+ },
67
+ "overrides": {
68
+ "rimraf": "^6.0.1"
69
+ }
70
+ }