Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ temp/

# Config files
.mock-config.json
docs/
13 changes: 9 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ TypeScript is strict (`noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexe

**Entry point**: `src/index.ts` — parses CLI args via Commander; if no args, runs the interactive `src/cli/wizard.ts`. Both paths call `startServer(config)`.

**Mock modes** (`mockMode: 'dev' | 'strict'`, default `'dev'`):
- `dev`: `statusOverride` and `latency` middlewares are mounted
- `strict`: those middlewares are not mounted at all — clean REST simulation
- Resolution order: CLI `--mock-mode` > `MOCK_API_MODE` env var > config file > default (`'dev'`)

**Request lifecycle** (`src/server.ts` → `src/core/router.ts`):
1. Express middleware chain: CORS → JSON → logger → `statusOverride` → optional latency
1. Express middleware chain: CORS → JSON → logger → `statusOverride` (dev only) → latency (dev only, if configured)
2. All non-system routes hit `dynamicRouteHandler` (catch-all `app.all('*')`)
3. Router calls `findTypeForUrl(url, typesDir)` to resolve a TypeScript interface name from the URL
4. Calls `generateMockFromInterface` or `generateMockArray` from `src/core/parser.ts`
Expand All @@ -47,11 +52,11 @@ TypeScript is strict (`noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexe
- After generation, `extractConstraints` parses the TypeScript AST (via `typescript` compiler API) for JSDoc annotations (`@min`, `@max`, `@minLength`, `@maxLength`, `@pattern`, `@enum`)
- `applyConstraintsToMock` in `src/core/constrainedGenerator.ts` then regenerates non-conforming fields using Faker

**Special headers**:
- `x-mock-status: <code>` — forces the response HTTP status code (handled by `src/middlewares/statusOverride.ts`)
**Special headers** (dev mode only):
- `x-mock-status: <code>` — forces the response HTTP status code (handled by `src/middlewares/statusOverride.ts`; ignored in `strict` mode by not mounting the middleware)

**System routes** (not matched by dynamic handler):
- `GET /health` — server status + cache stats
- `GET /api-docs` — Swagger UI (spec auto-regenerated on hot-reload file changes)

**Key types** (`src/types/config.ts`): `ServerConfig`, `RouteTypeMapping`, `InterfaceMetadata`, `ParsedSchema`, `MockGenerationOptions`
**Key types** (`src/types/config.ts`): `ServerConfig`, `MockMode`, `RouteTypeMapping`, `InterfaceMetadata`, `ParsedSchema`, `MockGenerationOptions`
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,21 @@ Options:
-t, --types-dir <path> Directory with TypeScript types (required)
-p, --port <number> Server port (default: 8080)
-l, --latency <range> Latency simulation "min-max" (e.g., 500-2000)
--mock-mode <strict|dev> Mock mode (default: dev)
--no-hot-reload Disable auto-reload on changes
--no-cache Disable schema caching
-v, --verbose Enable verbose logging
-h, --help Show help
```

**Environment Variables**

| Variable | Values | Description |
|---|---|---|
| `MOCK_API_MODE` | `strict`, `dev` | Override mock mode without CLI flag |

Resolution order: CLI `--mock-mode` > `MOCK_API_MODE` env var > config file > default (`dev`).

**Step 3: Call your API**

The server enforces idiomatic REST URL patterns:
Expand Down Expand Up @@ -297,6 +306,44 @@ export interface Product {

---

## Mock Modes

The server supports two modes controlled by `mockMode`:

| Mode | Description |
|---|---|
| `dev` (default) | All mock features enabled: `x-mock-status` header, artificial latency |
| `strict` | Clean REST simulation — mock features disabled, behaviour matches a real API |

In `strict` mode, the `statusOverride` and `latency` middlewares are not mounted at all.

```bash
# CLI
npx ts-mock-proxy --types-dir ./types --mock-mode strict

# Environment variable
MOCK_API_MODE=strict npx ts-mock-proxy --types-dir ./types
```

### Mock features (dev mode only, non-prod)

These features are active only in `dev` mode and should not be used to simulate real API behaviour:

**`x-mock-status`** — forces any response to return the specified HTTP status code:

```bash
# Force a 503 response
curl -H "x-mock-status: 503" http://localhost:8080/users
```

**Artificial latency** — simulates network delay (configured via `--latency` or the wizard):

```bash
npx ts-mock-proxy --types-dir ./types --latency 200-800
```

---

## How It Works

The server enforces idiomatic REST URL patterns. URLs are parsed into segments (stripping `api` and `v{n}` prefixes) and each segment is classified as either a **collection name** or an **ID** (numeric, UUID, or MongoDB ObjectId).
Expand Down
6 changes: 0 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion src/cli/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import inquirer from 'inquirer';
import * as path from 'path';
import * as fs from 'fs';
import chalk from 'chalk';
import { ServerConfig } from '../types/config';
import { ServerConfig, MockMode } from '../types/config';
import { logger } from '../utils/logger';
import { loadSavedConfig, saveConfig } from '../utils/configPersistence';

Expand Down Expand Up @@ -122,9 +122,20 @@ export async function runWizard(): Promise<ServerConfig> {
let latency: { min: number; max: number } | undefined = savedConfig?.latency;
let verbose = savedConfig?.verbose ?? false;
let writeMethods: ServerConfig['writeMethods'] = savedConfig?.writeMethods;
let mockMode: MockMode = savedConfig?.mockMode ?? 'dev';

if (advancedAnswer.showAdvanced) {
const advOptions = await inquirer.prompt([
{
type: 'list',
name: 'mockMode',
message: 'Mock mode:',
choices: [
{ name: 'dev — all mock features enabled (status override, artificial latency)', value: 'dev' },
{ name: 'strict — clean REST simulation, mock features disabled', value: 'strict' },
],
default: mockMode,
},
{
type: 'confirm',
name: 'hotReload',
Expand All @@ -151,6 +162,7 @@ export async function runWizard(): Promise<ServerConfig> {
},
]);

mockMode = advOptions.mockMode;
hotReload = advOptions.hotReload;
cache = advOptions.cache;
verbose = advOptions.verbose;
Expand Down Expand Up @@ -246,6 +258,7 @@ export async function runWizard(): Promise<ServerConfig> {
cache,
verbose,
writeMethods,
mockMode,
};

displayConfigSummary(config);
Expand Down Expand Up @@ -295,6 +308,7 @@ function displayConfigSummary(config: ServerConfig): void {
console.log(` ${chalk.cyan('Latency:')} ${config.latency.min}-${config.latency.max}ms`);
}

console.log(` ${chalk.cyan('Mock mode:')} ${config.mockMode ?? 'dev'}`);
console.log(` ${chalk.cyan('Hot-reload:')} ${config.hotReload ? 'enabled' : 'disabled'}`);
console.log(` ${chalk.cyan('Cache:')} ${config.cache ? 'enabled' : 'disabled'}`);
console.log(` ${chalk.cyan('Verbose:')} ${config.verbose ? 'enabled' : 'disabled'}`);
Expand Down
19 changes: 18 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { Command } from 'commander';
import { ServerConfig } from './types/config';
import { ServerConfig, MockMode } from './types/config';
import { startServer } from './server';
import { logger } from './utils/logger';
import { schemaCache } from './core/cache';
Expand All @@ -11,6 +11,18 @@ import { saveConfig } from './utils/configPersistence';

const program = new Command();

/**
* Resolves the mock mode from CLI arg or MOCK_API_MODE env var.
* Exits with a clear error if the value is invalid.
*/
function resolveMockMode(cliValue?: string): MockMode {
const value = cliValue ?? process.env['MOCK_API_MODE'];
if (!value) return 'dev';
if (value === 'strict' || value === 'dev') return value;
logger.error(`Invalid mockMode value: "${value}". Must be "strict" or "dev".`);
process.exit(1);
}

async function main() {
// Check if user provided explicit CLI arguments
const hasCliArgs = hasExplicitCliArgs();
Expand Down Expand Up @@ -38,6 +50,7 @@ async function main() {
.option('--no-hot-reload', 'Disable hot-reload of type definitions')
.option('--no-cache', 'Disable schema caching')
.option('-v, --verbose', 'Enable verbose logging', false)
.option('--mock-mode <strict|dev>', 'Mock mode: "dev" enables all mock features (default), "strict" disables them')
.option('--interactive', 'Force interactive mode')
.action(async (options) => {
// If --interactive flag is set, run wizard instead
Expand All @@ -61,6 +74,9 @@ async function main() {
latency = parseLatency(options.latency);
}

// Resolve mockMode: CLI > ENV > default
const mockMode = resolveMockMode(options.mockMode);

// Build the configuration
const config: ServerConfig = {
typesDir,
Expand All @@ -69,6 +85,7 @@ async function main() {
hotReload: options.hotReload !== false,
cache: options.cache !== false,
verbose: options.verbose,
mockMode,
};

// Configure the global cache
Expand Down
14 changes: 8 additions & 6 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export function createServer(config: ServerConfig): Express {
// Logging middleware
app.use(requestLoggerMiddleware);

// Status override middleware
app.use(statusOverrideMiddleware);
// Mock-only middlewares — only mounted in 'dev' mode
if ((config.mockMode ?? 'dev') === 'dev') {
app.use(statusOverrideMiddleware);

// Latency middleware (if configured)
if (config.latency) {
app.use(latencyMiddleware(config.latency.min, config.latency.max));
if (config.latency) {
app.use(latencyMiddleware(config.latency.min, config.latency.max));
}
}

// Health route
Expand Down Expand Up @@ -169,8 +170,9 @@ export function startServer(
const server = app.listen(config.port, () => {
logger.server(config.port);
logger.info(`Types directory: ${config.typesDir}`);
logger.info(`Mock mode: ${config.mockMode ?? 'dev'}`);

if (config.latency) {
if (config.latency && (config.mockMode ?? 'dev') === 'dev') {
logger.info(
`Latency simulation: ${config.latency.min}-${config.latency.max}ms`
);
Expand Down
13 changes: 13 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* Mock mode controlling which mock-only features are active
* - `dev`: all mock features enabled (status override, artificial latency)
* - `strict`: clean REST simulation, mock features disabled
*/
export type MockMode = 'strict' | 'dev';

/**
* TS-Mock-Proxy server configuration
*/
Expand All @@ -23,6 +30,12 @@ export interface ServerConfig {
/** Verbose mode for logging */
verbose: boolean;

/**
* Mock mode: 'dev' (default) enables all mock features; 'strict' disables them.
* Resolution order: CLI > MOCK_API_MODE env var > config file > default ('dev')
*/
mockMode?: MockMode;

/** Enable/disable write HTTP methods (POST, PUT, PATCH, DELETE). All enabled by default. */
writeMethods?: {
post?: boolean;
Expand Down
56 changes: 56 additions & 0 deletions tests/integration/server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,62 @@ describe('Server integration', () => {
});
});

// ---------------------------------------------------------------------------
// mockMode: strict
// ---------------------------------------------------------------------------
describe('mockMode: strict', () => {
const strictApp = createServer({
typesDir: FIXTURES_DIR,
port: 0,
hotReload: false,
cache: false,
verbose: false,
mockMode: 'strict',
});

it('ignores x-mock-status header and returns normal status', async () => {
const res = await request(strictApp)
.get('/api/users')
.set('x-mock-status', '503');
expect(res.status).toBe(200);
});

it('ignores x-mock-status on write methods', async () => {
const res = await request(strictApp)
.post('/api/users')
.set('x-mock-status', '409')
.send({ name: 'Alice' });
expect(res.status).toBe(201);
});

it('still serves normal mock data', async () => {
const res = await request(strictApp).get('/api/users');
expect(res.status).toBe(200);
expect(Array.isArray(res.body.data)).toBe(true);
});
});

// ---------------------------------------------------------------------------
// mockMode: dev (explicit)
// ---------------------------------------------------------------------------
describe('mockMode: dev', () => {
const devApp = createServer({
typesDir: FIXTURES_DIR,
port: 0,
hotReload: false,
cache: false,
verbose: false,
mockMode: 'dev',
});

it('applies x-mock-status header', async () => {
const res = await request(devApp)
.get('/api/users')
.set('x-mock-status', '503');
expect(res.status).toBe(503);
});
});

// ---------------------------------------------------------------------------
// Disabled write methods
// ---------------------------------------------------------------------------
Expand Down
Loading