plug-api-gateway
v0.1.0
Published
A configuration-first, production-ready API gateway for Node.js
Maintainers
Readme
plug-api-gateway
A plug-and-play Node.js gateway built around validated YAML configuration. Install it as a CLI product or embed it in a Fastify application. It provides versioned streaming proxy routes, OIDC/JWKS authentication, Redis-backed distributed rate limits, health probes, and structured operational logs.
Install and start
Requires Node.js 22 or newer and Redis.
npm install plug-api-gateway
npx plug-api-gateway init gateway.yaml
npx plug-api-gateway validate --config gateway.yaml
npx plug-api-gateway start --config gateway.yamlEdit the generated gateway.yaml to point at your identity provider, Redis, and upstream services. The gateway listens on port 3000 by default.
For a global installation:
npm install --global plug-api-gateway
plug-api-gateway start --config /etc/plug-api-gateway/gateway.yamlinit refuses to replace an existing file unless --force is supplied. GATEWAY_CONFIG_FILE can provide the default file for start and validate.
Embed in Node.js
The package has no import-time network, signal, or process side effects:
import { createGateway } from 'plug-api-gateway';
const gateway = await createGateway({ config: './gateway.yaml' });
await gateway.start();
process.once('SIGTERM', () => {
void gateway.close();
});createGateway owns its Redis connection and closes it with the Fastify server. Advanced consumers can inject ApplicationDependencies or use buildApplication directly for custom rate-limit, health, and token-verification integrations. Public TypeScript types and the validated gatewayConfigSchema are exported from the package root.
Develop locally
npm install
cp config/gateway.example.yaml config/gateway.yaml
npm run devSet GATEWAY_CONFIG_FILE to load a different YAML file. Configuration values support ${NAME} and ${NAME:-default} interpolation. Startup fails when a required variable is absent or the resulting configuration is invalid.
Versioned routes
Each route declares a version and resource prefix separately:
- id: users
version: v1
prefix: /users
upstream: http://users-service:8080
allowedMethods: [GET, POST]This route is exposed at /api/v1/users. Versions use v1, v2, and so on. Prefixes are normalized by validation: they begin with /, have no trailing or repeated slash, and reject traversal and encoded separator forms. Duplicate route IDs and duplicate versioned prefixes are rejected at startup. Nested prefixes are allowed; lookup chooses the longest match on a complete path-segment boundary.
Deprecated routes may declare deprecatedAt, sunsetAt, and successorUrl. The gateway then emits Deprecation, Sunset, and Link: <...>; rel="successor-version" headers. Both dates use ISO 8601 in YAML, sunset must follow deprecation, and all three values are required together.
The current runnable endpoints are:
GET /health/liveGET /health/ready
Proxy behavior
Matched requests are streamed to their configured upstream with query strings intact. stripPrefix defaults to true, so /api/v1/users/42 reaches the upstream as /42; set it to false to preserve the complete public path. allowedMethods is enforced before forwarding.
server.upstreamTimeoutMs sets the default upstream timeout and timeoutMs overrides it for one route. server.bodyLimitBytes caps request bodies. The gateway removes hop-by-hop headers, replaces forwarding headers using Fastify's trusted-proxy interpretation, and propagates or generates X-Request-ID. Only enable trustProxy for known proxy addresses or CIDR ranges.
Errors use one shape:
{"code":"ROUTE_NOT_FOUND","message":"No route matches this request","requestId":"req-1"}Routing failures return 404 or 405, oversized bodies return 413, upstream connection failures return 502, and upstream timeouts return 504.
Authentication and scopes
OIDC configuration pins the issuer, one or more accepted audiences, the JWKS URL, and an explicit asymmetric algorithm allowlist. Symmetric HS* algorithms and unsecured tokens are rejected by configuration. Remote keys are cached by jose and refreshed when signing keys rotate.
Routes choose one authorization mode:
publicaccepts requests without a token.authenticatedrequires a valid bearer JWT.scopedalso requires every entry inrequiredScopes.
Bearer syntax is strict. Verification covers signature, key/algorithm compatibility, issuer, audience, expiration, and not-before claims. Configure scopeRepresentation as space-delimited-string for claims such as "users:read users:write", or string-array for claims such as ["users:read", "users:write"]. A mismatched claim type is denied rather than coerced.
Authentication failures return 401 with WWW-Authenticate; valid tokens lacking scopes return 403. Responses and logs do not expose token validation details.
Rate limits and health
Redis provides a distributed fixed-window counter. The global rateLimit policy applies unless a route supplies an override. Keys combine route ID with authenticated sub, falling back to Fastify's trusted client IP when no subject exists. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; rejected requests also include Retry-After.
Rate limiting fails closed with 503 if Redis cannot check a counter. Redis availability does not prevent process startup: /health/live remains 200, while /health/ready returns 503 until Redis responds and automatically recovers to 200 afterward.
Logs are Pino JSON at logging.level. Authorization, cookies, proxy authorization, set-cookie, and configured sensitive headers are redacted. Request summaries include request ID, route ID, method, status, latency, and authenticated subject. SIGTERM and SIGINT stop new traffic, drain Fastify requests, then close Redis.
Docker demo
The Compose environment builds the production image and starts Redis, a local JWKS/token fixture, mock upstream endpoints, and the gateway:
docker compose up --build --wait --detach
sh tools/compose-smoke.sh
docker compose down --volumesThe smoke test checks liveness, readiness, public versioned proxying, a scoped request with a freshly signed demo token, rate limiting, and graceful gateway stop. The fixture key and tokens are ephemeral and only suitable for local demonstrations.
The smoke script prefers the docker compose plugin and falls back to the standalone docker-compose command. Set COMPOSE_COMMAND to exactly docker compose or docker-compose to select one explicitly. On older installations, substitute docker-compose for each Compose command shown above.
If host port 3000 is occupied, choose another host port while keeping the container port unchanged, then point the smoke script at it:
GATEWAY_PORT=3100 docker-compose up --build --detach
GATEWAY_URL=http://localhost:3100 COMPOSE_COMMAND=docker-compose sh tools/compose-smoke.shOperations and security
- Terminate TLS at a trusted ingress and configure
trustProxyonly for known addresses or CIDR ranges. - Keep upstream targets and the JWKS URI under deployment control; clients cannot choose proxy destinations.
- Use
rediss://and authenticated Redis in production. A Redis outage makes readiness fail and proxy requests fail closed. - Monitor readiness,
5xxresponses, rate-limit rejections, upstream latency, and JWKS retrieval failures. - Roll out successor API versions before publishing deprecation and sunset dates.
- The container runs as the unprivileged
nodeuser and exposes only port 3000.
Quality checks
npm run check
npm run pack:checkThese run ESLint, Vitest, the strict TypeScript build, and an npm package-content preview. Before publishing, run npm pack, install the generated tarball into a clean temporary project, and verify the CLI there.
Publish a release
The working package name is plug-api-gateway; it was unregistered when selected, but npm availability must be checked again immediately before the first release. Complete the repository, author, support, and homepage fields once permanent project URLs exist.
npm login
npm view plug-api-gateway
npm run check
npm run pack:check
npm publish --provenancePublishing requires an npm account with two-factor authentication. prepublishOnly runs the complete quality suite automatically. Use semantic versions and publish from a clean, tagged Git commit once the repository is established.
Container validation, when Docker is available:
docker compose config --quiet
docker compose build
docker compose up --wait --detach
sh tools/compose-smoke.sh
docker compose down --volumesA CI system should run those commands after npm ci and npm run check. No workflow is included because this workspace is not currently a Git repository and its eventual repository root is unknown.
docker-compose config --quiet, image build, and the full runtime smoke test have been verified in the current workspace. The runtime check used GATEWAY_PORT=3100 with GATEWAY_URL=http://localhost:3100 and covered liveness, readiness, public versioned proxying, scoped JWT access, Redis rate limiting, graceful gateway stop, and volume cleanup.
Design direction
Configuration loading, token verification, route lookup, rate limiting, and dependency health use narrow interfaces. Comments are reserved for security rules or intent that names and types cannot express.
