Skip to content

anotherbuginthecode/bun-gateway

Repository files navigation

bun-gateway

A lightweight, configurable API gateway built on Bun. It sits in front of your backend services and provides auth, rate limiting, request/response transformation, and structured logging through a composable plugin pipeline.

Features

  • JWT and API key authentication per route
  • Redis-backed rate limiting with atomic fixed-window counters (safe for multi-instance deployments, fails open if Redis is unavailable)
  • Path rewriting — strip the route prefix before forwarding
  • Header sanitization — strips x-powered-by, server, and spoofable trust headers; injects x-request-id, x-user-id, x-forwarded-for
  • Upstream timeout — configurable via UPSTREAM_TIMEOUT_MS (default 30 s); slow upstreams return 504 instead of hanging indefinitely
  • Structured JSON logging to stdout (newline-delimited, ready for log aggregators)
  • Dynamic route configuration via a mounted routes.yaml file — no rebuild required

Architecture

Each request flows through a composed plugin chain:

logger → auth → rate-limit → proxy

Every plugin has the signature:

(ctx: GatewayContext, next: () => Promise<Response>) => Promise<Response>;

Calling next() advances the chain. Not calling it short-circuits (e.g. rate-limit returns 429 directly). The logger sits outermost so it always captures the final status and duration. The proxy is the terminal handler and never calls next().

GatewayContext

Shared state passed through the pipeline for each request:

Field Type Description
req Request Original incoming request
route IRoute Matched route config
claims JWTPayload | null JWT claims (set by auth plugin)
clientId string Identifies the caller for rate limiting
rateInfo RateInfo Remaining quota, reset time
requestId string UUID injected as x-request-id
startedAt number performance.now() at request start

Environment Variables

Variable Required Default Description
JWT_SECRET Yes* Secret for HS256 JWT verification
API_KEY Yes* Comma-separated list of valid API keys
PORT No 3000 Listening port
REDIS_URL No redis://localhost:6379 Redis connection URL
ROUTES_FILE No Path to a routes.yaml file. If set, routes are loaded from this file instead of the built-in routes.ts
UPSTREAM_TIMEOUT_MS No 30000 Upstream fetch timeout in ms. Returns 504 Gateway Timeout if the backend does not respond in time

*Required only if at least one route uses jwt or apikey auth respectively.


Route Configuration

Routes can be defined in two ways:

Option A — Static (local dev)

Edit src/routing/routes.ts directly:

export const routes: IRoute[] = [
  {
    prefix: "/api/users",
    target: "http://users-service:3001",
    auth: "jwt",
    rateLimit: { window: 60_000, max: 100 },
  },
];

Option B — Dynamic via routes.yaml (Docker / Kubernetes)

Set the ROUTES_FILE environment variable to the path of a YAML file. The gateway loads it at startup and fails hard if the file is missing or invalid.

Full example

routes:
  # JWT-authenticated route with prefix stripping and rate limiting
  - prefix: /api/users
    target: http://users-service:3001
    auth: jwt
    rewrite:
      stripPrefix: true # /api/users/123 → /123
    rateLimit:
      window: 60000 # milliseconds
      max: 100 # requests per window per client

  # API key authenticated route, path forwarded as-is
  - prefix: /api/orders
    target: http://orders-service:3002
    auth: apikey
    rateLimit:
      window: 60000
      max: 50

  # Unauthenticated public route with prefix stripping
  - prefix: /public
    target: http://static-service:8080
    auth: none
    rewrite:
      stripPrefix: true

Route fields

Field Required Description
prefix Yes URL prefix to match. Must start with /
target Yes Backend base URL (http:// or https://)
auth Yes jwt, apikey, or none
rewrite.stripPrefix No If true, strips the route prefix from the forwarded path
rateLimit.window No Time window in milliseconds
rateLimit.max No Max requests per client per window

Routes are matched in order — first prefix match wins.


Running Locally

# Install dependencies
bun install

# Start with hot reload
bun run dev

The gateway listens on http://localhost:3000 by default. A health check endpoint is available at GET /health.


Docker Compose

Mount your routes.yaml as a read-only volume and pass the path via ROUTES_FILE:

services:
  gateway:
    build: .
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      JWT_SECRET: your-secret-here
      API_KEY: key1,key2
      REDIS_URL: redis://redis:6379
      ROUTES_FILE: /etc/gateway/routes.yaml
    volumes:
      - ./routes.yaml:/etc/gateway/routes.yaml:ro
    depends_on:
      - redis

  redis:
    image: redis:7-alpine

To update routes, replace routes.yaml and restart the gateway container. No rebuild is needed.


Kubernetes

Store your routes as a ConfigMap and mount it into the gateway pod:

apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-routes
data:
  routes.yaml: |
    routes:
      - prefix: /api/users
        target: http://users-service:3001
        auth: jwt
        rewrite:
          stripPrefix: true
        rateLimit:
          window: 60000
          max: 100
      - prefix: /api/orders
        target: http://orders-service:3002
        auth: apikey
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      containers:
        - name: gateway
          image: your-registry/ts-gateway:latest
          ports:
            - containerPort: 3000
          env:
            - name: PORT
              value: "3000"
            - name: ROUTES_FILE
              value: /etc/gateway/routes.yaml
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: gateway-secrets
                  key: jwt-secret
            - name: API_KEY
              valueFrom:
                secretKeyRef:
                  name: gateway-secrets
                  key: api-key
            - name: REDIS_URL
              value: redis://redis:6379
          volumeMounts:
            - name: routes-config
              mountPath: /etc/gateway
      volumes:
        - name: routes-config
          configMap:
            name: gateway-routes

To update routes, apply the updated ConfigMap and roll the deployment:

kubectl apply -f configmap.yaml
kubectl rollout restart deployment/gateway

Extending the Gateway

Add a plugin: implement the Plugin type and insert it into the createPipeline([...]) call in src/index.ts. Request-handling logic (routing, error mapping) lives in src/handler.ts.

import type { Plugin } from "./pipeline/pipeline";

export const myPlugin: Plugin = async (ctx, next) => {
  // pre-processing
  const response = await next();
  // post-processing
  return response;
};

Add a static route: append an entry to src/routing/routes.ts.


Commands

bun run dev     # Start with hot reload
bun test        # Run tests (env vars set automatically via preload)
bun run build   # Build
bun run clean   # Remove dist/

About

A lightweight API gateway built on Bun — JWT/API key auth, Redis rate limiting, and reverse proxy in a composable plugin pipeline.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors