A production-shaped API Gateway built in strict-mode TypeScript. It sits in front of independent backend microservices, handling authentication, rate limiting, request routing, and asynchronous analytics — the kind of infrastructure most applications depend on but rarely build from scratch.
This project began as a JavaScript implementation and was fully rewritten in strict TypeScript to deepen understanding of type-safe backend architecture.
Client
│
▼
Rate Limiter (Redis token-bucket-style counter, fail-open on Redis errors)
│
▼
Logger (pushes async job to BullMQ queue, non-blocking)
│
▼
JWT Auth Middleware (per-route, validates token, attaches req.user)
│
▼
Reverse Proxy (http-proxy-middleware, path-rewritten per service)
│
├──▶ Service A — Users API (port 4001)
└──▶ Service B — Products API (port 4002)
Meanwhile, asynchronously:
Logger job ──▶ BullMQ Worker ──▶ MongoDB (persisted log)
│
▼
Recalculates live stats
│
▼
Socket.io ──▶ Real-time dashboard
- JWT Authentication — token verification middleware with distinct handling for expired vs. malformed tokens, and safe attachment of decoded user data to the request object via TypeScript declaration merging.
- Redis-backed Rate Limiting — per-client request counting with automatic expiry, and a deliberate fail-open policy: if Redis itself becomes unavailable, requests are still allowed through rather than blocking all traffic.
- Reverse Proxy Routing — routes requests to independent backend services using
http-proxy-middleware, with custompathRewritelogic to correctly restore the service-specific path after Express strips the mount prefix. - Asynchronous Logging — every request is logged via a BullMQ job queue rather than blocking the response cycle. A dedicated worker process consumes these jobs and persists them to MongoDB.
- Real-Time Analytics Dashboard — a MongoDB aggregation pipeline computes live traffic metrics (total requests, requests/minute, average response time, error rate, top routes), broadcast to connected dashboard clients over Socket.io on every processed log.
- Strict TypeScript throughout — no implicit
any, fullstrictmode, custom type declarations extending Express'sRequesttype, and generic typing on BullMQ workers and queues.
| Layer | Technology |
|---|---|
| Language | TypeScript (strict mode, nodenext module resolution) |
| Web Framework | Express.js |
| Auth | JSON Web Tokens (jsonwebtoken) |
| Rate Limiting / Caching | Redis (ioredis) |
| Job Queue | BullMQ |
| Database | MongoDB (Mongoose) |
| Real-time | Socket.io |
| Reverse Proxy | http-proxy-middleware |
API-GATEWAY-TS-/
├── src/
│ ├── middleware/
│ │ ├── auth.ts # JWT verification middleware
│ │ ├── rateLimiter.ts # Redis-backed rate limiting
│ │ ├── logger.ts # Async request logging via BullMQ
│ │ └── proxyMiddleware.ts # Reverse proxy configs for each service
│ ├── services/
│ │ ├── service_A/ # Users microservice
│ │ ├── service_B/ # Products microservice
│ │ └── statsService.ts # Shared analytics aggregation logic
│ ├── worker/
│ │ ├── index.ts # BullMQ worker + Socket.io dashboard server
│ │ └── schema/dbSchema.ts # Mongoose log schema
│ ├── config.ts # Environment variable loading
│ ├── generateToken.ts # Dev utility for issuing test JWTs
│ └── server.ts # Main gateway entry point
├── types/
│ └── express.d.ts # Declaration merging for req.user
├── tsconfig.json
└── package.json
- Fail-open rate limiting: prioritizes availability over strict enforcement — if the rate limiter's own dependency (Redis) fails, the system doesn't cascade that failure onto every client request.
- Path rewriting: Express strips the mount path before handing a request to proxy middleware.
pathRewriteexplicitly re-prepends the correct service path (/users,/products) rather than assuming the path arrives unchanged. - Declaration merging over
any: rather than castingreqtoanyto attach authenticated user data, the project extends Express's ownRequestinterface via a dedicated.d.tsfile — preserving full type safety on every downstream access toreq.user. - Decoupled logging: request logging happens through a queue, not inline in the middleware chain, so a slow or failing log write never adds latency to the actual client-facing request.
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env
# fill in JWT_SECRET, MONGO_URI, REDIS_HOST, REDIS_PORT, PORT1, PORT2, PORT
# Run in development
npm run devPrerequisites: Redis and MongoDB running locally (or accessible via the connection details in .env).
Converting this project to strict TypeScript surfaced real architectural questions beyond syntax: how to safely extend third-party library types via declaration merging, when a double type-cast through unknown is a legitimate tool versus a red flag, how contextual typing works when passing callbacks into library functions like createProxyMiddleware, and why environment variable loading order matters under ES modules. Debugging the path-rewriting behavior in particular required tracing actual request behavior through Express's internals rather than trusting assumptions about how the path arrives at the proxy layer.
MIT