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.localthen.env.default).
- Quick Start
- Environment Variables
- Rule System
- Examples
- Built‑in Endpoints
- Development Workflow
- Project Structure
- Common Scenarios
- Security Notes
- Extension Ideas
- Rule Organization & Archival
- Comparison with MSW
- License
pnpm install
pnpm devSmoke test:
curl http://localhost:4000/__envLoad 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.defaultis committed and should remain non-sensitive.
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:
- Overload form:
rule(method: string | string[], path: string | RegExp, handler, options?)- Config object form:
rule({ path?: string|RegExp, test?: (req)=>boolean, methods?: string[], name?, enabled?, handler })Constraints:
- Provide either
pathortest(if both given,testaugments path match logic you control). - If
methodsomitted in config form it defaults to["GET"]. - First matching enabled rule short-circuits.
Export patterns:
export const SomeRule = rule(...)(recommended; export name becomes rule name)- Multiple named exports per file (all collected)
- Legacy:
export default rule(...)orexport const rules = [ rule(...), ... ](still supported) - 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
nameset insiderule()options (thenameoption is deprecated).
import { rule } from "../utils.js";
export default rule({
name: "ping",
path: "/__ping",
methods: ["GET"],
handler: (_req, res) => res.json({ ok: true, t: Date.now() }),
});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" });
},
});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() }),
}),
];export default rule({
name: "temp-off",
path: "/disabled",
enabled: false,
handler: (_r, res) => res.json({ off: true }),
});| 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.
- Add / edit files under
rules/ - Save → nodemon restarts
- Inspect startup log for loaded overrides
- Send requests to validate
Change upstream: set PROXY_TARGET in .env.local
Restrict CORS: CORS_ORIGINS=http://localhost:3000,https://dev.example.com
.
├─ main.ts
├─ utils.ts
├─ rules/
│ └─ _demo.ts
├─ .env.default
├─ package.json
├─ tsconfig.json
└─ nodemon.json
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"
- Keep secrets only in
.env.local. - Remove or protect
/__envif exposing externally. - Rules execute arbitrary code: review sources.
- Avoid exposing this service directly to the public Internet.
| 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 |
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.
- Group by feature / domain / scenario using either subfolders and/or multi-export files.
- Example: consolidate stable org endpoints into
rules/commerce/org1.tswith several named exports, and chat endpoints intorules/commerce/chat.ts.
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.
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).
- 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.
- For scratch work you do not want loaded or committed, use a dot-prefixed folder:
rules/.wip/. - Optionally list it in
.gitignoreso accidental commits are avoided.
- 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.
| 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 |
This keeps the runtime loader trivial (no registry/state) while still giving coarse-grained enable/disable. Git diffs also remain obvious.
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 |
- Override‑first with transparent passthrough: author only what you need to change; everything else stays real, reducing maintenance & data drift.
- Cross‑client sharing: any device or language adopts overrides by switching a base URL (or system proxy).
- Low intrusion: no library embedded in the app—easy to adopt or discard.
- Real network conditions: genuine CORS, cookies, caching, TLS; good for integration sanity checks.
- Flexible rules: an override is just an Express handler—inject latency, errors, dynamic data, conditional passthrough.
- Layered env loading: safe defaults in
.env.default, secrets in.env.local(git‑ignored). - Evolution friendly: ideal anchor point for future record & replay, metrics, runtime toggles, chaos/fault injection, priority control.
- Short learning curve: minimal API (
rule()+ file export); experienced Node/Express users are productive immediately.
- Day-to-day team development: run
override-proxyfor shared partial overrides + live upstream behavior. - Test / CI: use MSW for 100% deterministic, offline, fast tests.
- Demo / Storybook: point at
override-proxyfor realistic hybrid data; fall back to MSW when full offline determinism needed.
Summary:
override-proxyis a shared, real-network, partial-override layer; MSW is an in-process, fully controllable interception layer. They complement rather than exclude each other.
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;
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
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.