Skip to content

Pluggable local development server that serves rule-based overrides first, then proxies unmatched requests to an upstream PROXY_TARGET.

License

Notifications You must be signed in to change notification settings

crescendolab-open/override-proxy

Repository files navigation

override-proxy

Pluggable local development server that serves rule-based overrides first, then proxies unmatched requests to an upstream PROXY_TARGET.

Key features:

  • Override-first: if a rule matches, respond immediately; otherwise proxy.
  • Dynamic rule loading from rules/ (restart handled by nodemon).
  • Layered environment loading via dotenvx (.env.local then .env.default).

Table of Contents

  1. Quick Start
  2. Environment Variables
  3. Rule System
  4. Examples
  5. Built‑in Endpoints
  6. Development Workflow
  7. Project Structure
  8. Common Scenarios
  9. Security Notes
  10. Extension Ideas
  11. Rule Organization & Archival
  12. Comparison with MSW
  13. License

1. Quick Start

pnpm install
pnpm dev

Smoke test:

curl http://localhost:4000/__env

2. Environment Variables

Load order (first wins, no overwrite): .env.local.env.default

Sample .env.default (do not put secrets here):

PROXY_TARGET=https://pokeapi.co/api/v2/
PORT=4000
# CORS_ORIGINS=http://localhost:3000,https://your-app.local
Name Description Default
PROXY_TARGET Upstream target when no rule matches https://pokeapi.co/api/v2/
PORT Preferred port (auto-increments if busy) 4000
CORS_ORIGINS Allowed origins (comma list, empty = allow all) (empty)

Put secrets only in .env.local (ignored by git). .env.default is committed and should remain non-sensitive.

3. Rule System

Interface:

interface OverrideRule {
  name?: string;
  enabled?: boolean; // default true
  methods: [Method, ...Method[]]; // non-empty, uppercase
  test(req: Request): boolean;
  handler(
    req: Request,
    res: Response,
    next: NextFunction,
  ): void | Promise<void>;
}

Helper creation styles:

  1. Overload form:
rule(method: string | string[], path: string | RegExp, handler, options?)
  1. Config object form:
rule({ path?: string|RegExp, test?: (req)=>boolean, methods?: string[], name?, enabled?, handler })

Constraints:

  • Provide either path or test (if both given, test augments path match logic you control).
  • If methods omitted in config form it defaults to ["GET"].
  • First matching enabled rule short-circuits.

Export patterns:

  1. export const SomeRule = rule(...) (recommended; export name becomes rule name)
  2. Multiple named exports per file (all collected)
  3. Legacy: export default rule(...) or export const rules = [ rule(...), ... ] (still supported)
  4. Any other named export that is an OverrideRule (or an array of them) will be loaded.

Naming: when using named exports, the export identifier overrides any name set inside rule() options (the name option is deprecated).

4. Examples

4.1 Simple path

import { rule } from "../utils.js";
export default rule({
  name: "ping",
  path: "/__ping",
  methods: ["GET"],
  handler: (_req, res) => res.json({ ok: true, t: Date.now() }),
});

4.2 RegExp capture

import { rule } from "../utils.js";
export default rule({
  name: "user-detail",
  path: /^\/api\/users\/(\d+)$/,
  methods: ["GET"],
  handler: (req, res) => {
    const id = req.path.match(/^\/api\/users\/(\d+)$/)![1];
    res.json({ id, name: `User ${id}`, from: "override" });
  },
});

4.3 Custom test

import { rule } from "../utils.js";
export const rules = [
  rule({
    name: "feature-core",
    test: (req) =>
      req.method === "GET" &&
      req.path === "/feature-controls" &&
      req.query["only"] === "core",
    handler: (_req, res) =>
      res.json({ features: ["core-a", "core-b"], ts: Date.now() }),
  }),
];

4.4 Disabled rule

export default rule({
  name: "temp-off",
  path: "/disabled",
  enabled: false,
  handler: (_r, res) => res.json({ off: true }),
});

5. Built-in Endpoints

Path Method Description
/__env GET Basic non-sensitive environment info
* ANY Proxy fallback

Logging pattern: [id] -> METHOD path / match ruleName / completion line with status & source.

6. Development Workflow

  1. Add / edit files under rules/
  2. Save → nodemon restarts
  3. Inspect startup log for loaded overrides
  4. Send requests to validate

Change upstream: set PROXY_TARGET in .env.local
Restrict CORS: CORS_ORIGINS=http://localhost:3000,https://dev.example.com

7. Project Structure

.
├─ main.ts
├─ utils.ts
├─ rules/
│  └─ _demo.ts
├─ .env.default
├─ package.json
├─ tsconfig.json
└─ nodemon.json

8. Common Scenarios

Simulate latency: await new Promise(r => setTimeout(r, 800));
Conditional pass-through: handler: (req,res,next)=> req.query["passthrough"]? next(): res.json({x:1})
Header trigger: test: (req)=> req.headers["x-mock-mode"] === "1"

9. Security Notes

  • Keep secrets only in .env.local.
  • Remove or protect /__env if exposing externally.
  • Rules execute arbitrary code: review sources.
  • Avoid exposing this service directly to the public Internet.

10. Extension Ideas

Feature Description
/__rules List rules + status + hit counts
Runtime toggle Enable/disable via PATCH
Hot replace chokidar-based in-process swap
Fault / delay injection Simulate 4xx/5xx/timeout
Stats hit count / last hit timestamp
Priority control Explicit rule ordering

11. Rule Organization & Archival

You can treat rules/ as a shared, modular catalog of partial overrides. Simple conventions keep it clean and make whole scenario packs easy to toggle.

11.1 Group Related Rules

  • Group by feature / domain / scenario using either subfolders and/or multi-export files.
  • Example: consolidate stable org endpoints into rules/commerce/org1.ts with several named exports, and chat endpoints into rules/commerce/chat.ts.

11.2 Disable an Entire Group (Dot‑prefix)

The loader ignores dotfiles & dotfolders. Rename a folder to start with . to deactivate every rule inside without deleting them:

rules/demo-onboarding/    # active
rules/.demo-onboarding/   # inactive (ignored)

Remove the leading dot to reactivate.

11.3 Archive Packs in .trash

Move old / seldom used sets into rules/.trash/<pack>/. Because .trash begins with a dot, all contents are ignored.

rules/.trash/legacy-campaign/*

Bring them back by moving the folder out (and removing any leading dot).

11.4 Shareable by Design

  • Committed (non-sensitive) rule files are instantly shared—teammates restart and get the same overrides.
  • Avoid secrets / PII in responses. Use env vars or synthetic placeholders if needed.
  • Scenario-oriented packs let you prepare multiple demo states and enable exactly one (or a few) by folder name.

11.5 Personal / WIP Rules

  • For scratch work you do not want loaded or committed, use a dot-prefixed folder: rules/.wip/.
  • Optionally list it in .gitignore so accidental commits are avoided.

11.6 Naming Guidance

  • Folder names: concise, kebab-case domain or scenario (billing-refunds, chat-surge-test).
  • Rule name (shown in logs): stable identifier (PascalCase or kebab-case) reflecting purpose.

11.7 Quick Lifecycle Table

Action Steps
Add feature pack Create folder, add rule files, commit
Temporarily hide Rename folder to .folderName
Archive Move into rules/.trash/<folder>
Restore Move back / remove leading dot
Share Push & teammates restart proxy

11.8 Why Folders over Config Flags?

This keeps the runtime loader trivial (no registry/state) while still giving coarse-grained enable/disable. Git diffs also remain obvious.

12. Comparison with MSW

override-proxy and MSW both solve API interception/mocking but sit at different layers: this project is a standalone reverse proxy that applies override rules first and transparently forwards the rest; MSW runs inside your runtime (Service Worker in the browser or a Node process). They are often complementary (team‑wide shared partial overrides via override-proxy; fully deterministic isolated tests & Storybook via MSW).

Aspect override-proxy MSW When to favor override-proxy When to favor MSW
Deployment form Standalone Node reverse proxy In-process (Service Worker / Node) Need one shared layer for Web, Mobile, backend scripts Only JS app/tests, want zero base URL changes
Override strategy First matching rule short-circuits, rest passthrough All requests potentially intercepted; passthrough needs opting in Partial mock + keep real behavior for the rest Fully controlled, offline, deterministic data
Upstream realism Unmatched hits real upstream (reduced mock drift) All data must be defined/generative Want to reduce divergence between mock and prod Want fully stable replayable fixtures
Team sharing Point base URL; everyone instantly uses same overrides Must add handlers per repo Fast alignment “what’s overridden today” Single codebase control is enough
Client languages Any (JS, iOS, Android, backend) via HTTP Primarily JavaScript ecosystems Multi-language integration workflows Pure JS/UI workflows
Logging & observability Centralized request log (latency, status, source, rule) Distributed per environment Need mixed real+mock traffic insight Local test verbosity sufficient
CORS / network semantics Real browser/network semantics preserved Simulated inside SW/Node Need to validate real cookies/CORS/TLS Network realism not required
Adoption cost Run one process + point base URL Install lib + configure handlers in each env Want zero code intrusion Prefer inline mocks in tests
Extensibility surface Natural spot for caching, record/replay, fault/latency injection Built-in REST/GraphQL/WebSocket already Need proxy aggregation / caching Need protocol breadth immediately
Non-JS test integration Any stack via HTTP Requires JS runtime Mixed polyglot E2E JS-only test matrix

Key strengths of this project

  1. Override‑first with transparent passthrough: author only what you need to change; everything else stays real, reducing maintenance & data drift.
  2. Cross‑client sharing: any device or language adopts overrides by switching a base URL (or system proxy).
  3. Low intrusion: no library embedded in the app—easy to adopt or discard.
  4. Real network conditions: genuine CORS, cookies, caching, TLS; good for integration sanity checks.
  5. Flexible rules: an override is just an Express handler—inject latency, errors, dynamic data, conditional passthrough.
  6. Layered env loading: safe defaults in .env.default, secrets in .env.local (git‑ignored).
  7. Evolution friendly: ideal anchor point for future record & replay, metrics, runtime toggles, chaos/fault injection, priority control.
  8. Short learning curve: minimal API (rule() + file export); experienced Node/Express users are productive immediately.

Typical combined workflow with MSW

  • Day-to-day team development: run override-proxy for shared partial overrides + live upstream behavior.
  • Test / CI: use MSW for 100% deterministic, offline, fast tests.
  • Demo / Storybook: point at override-proxy for realistic hybrid data; fall back to MSW when full offline determinism needed.

Summary: override-proxy is a shared, real-network, partial-override layer; MSW is an in-process, fully controllable interception layer. They complement rather than exclude each other.

Architecture & Flow (Mermaid)

flowchart LR
  subgraph Client
    A[Request]
  end
  A --> B[override-proxy]
  B -->|rule match| C[Override handler]
  B -->|no match| U[(Upstream API)]
  C --> R[Response]
  U --> R
  R --> A
  %% Behaviors: dynamic JSON, latency, error injection
  classDef proxy fill:#0d6efd,stroke:#084298,stroke-width:1px,color:#fff;
  class B proxy;
Loading

Complementary Usage with MSW

sequenceDiagram
  participant DevApp as Frontend App
  participant OP as override-proxy
  participant Up as Upstream API
  participant MSW as MSW (test env)

  Note over DevApp,OP: Local dev (shared partial overrides)
  DevApp->>OP: GET /api/items
  OP->>OP: Match rule?
  alt Rule matches
    OP-->>DevApp: Mocked JSON
  else No match
    OP->>Up: Forward request
    Up-->>OP: Real response
    OP-->>DevApp: Real JSON
  end
  Note over DevApp,MSW: Test/CI (fully mocked)
  DevApp->>MSW: GET /api/items
  MSW-->>DevApp: Deterministic mocked JSON
Loading

13. License

Apache License 2.0 © 2025 Crescendo Lab. See LICENSE for full text.


Author: Crescendo Lab — 2025

Need extras (rule listing, runtime toggles, latency/error injection)? Open an issue or ask.

About

Pluggable local development server that serves rule-based overrides first, then proxies unmatched requests to an upstream PROXY_TARGET.

Topics

Resources

License

Stars

Watchers

Forks