@zze/mock-server
v0.5.0
Published
A reusable Node.js mock server for microfrontend development
Maintainers
Readme
@zze/mock-server
A reusable Node.js mock server for simulating backend APIs during local microfrontend development.
Features
- 🚀 Zero-config TypeScript support - Write configs in JSON, JS, or TS
- 🔄 Hot reload - Automatic config reloading on file changes
- 🎭 Scenarios - Define multiple response scenarios per endpoint
- 🌊 Flows - Activate groups of scenarios to simulate user journeys
- 📁 File download - Serve binary files (PDFs, images, CSVs) from scenarios
- 📤 File upload - Accept
multipart/form-dataupload requests out of the box - 🎨 Admin UI - Visual dashboard at
/mock-adminto control the server - 🔌 REST API - Programmatic control via admin endpoints
- 📦 Single package - Everything bundled, no external dependencies needed
Installation
npm install --save-dev @zze/mock-serverQuick Start
1. Create your config directory structure
your-project/
├── mock-config/
│ ├── endpoints/
│ │ └── users.json
│ └── flows/
│ └── error-flow.json
└── package.json2. Define an endpoint
mock-config/endpoints/users.json
{
"id": "get-users",
"path": "/users",
"method": "GET",
"defaultScenarioId": "success",
"scenarios": [
{
"id": "success",
"name": "Success Response",
"status": 200,
"body": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
},
{
"id": "empty",
"name": "Empty List",
"status": 200,
"body": []
},
{
"id": "error",
"name": "Server Error",
"status": 500,
"body": { "error": "Internal server error" }
}
]
}3. Start the server
Option A: Programmatic (recommended)
// scripts/mock-server.ts
import { MockServer } from '@zze/mock-server';
const server = new MockServer({
configPath: './mock-config',
port: 3001,
apiPrefix: '/api',
hotReload: true
});
await server.start();
console.log('🎭 Mock server running at http://localhost:3001');
console.log('📊 Admin UI at http://localhost:3001/mock-admin');Run with:
npx tsx scripts/mock-server.tsOption B: Simple script in package.json
{
"scripts": {
"mock": "tsx scripts/mock-server.ts"
}
}4. Make requests
# Default scenario
curl http://localhost:3001/api/users
# → [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
# Open admin UI to switch scenarios
open http://localhost:3001/mock-adminConfiguration
Endpoint Config
Each endpoint file defines an API endpoint with multiple response scenarios.
interface EndpointConfig {
id: string; // Unique identifier
path: string; // URL path (supports Express patterns)
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
defaultScenarioId: string; // ID of the default scenario
scenarios: ScenarioConfig[]; // Available response scenarios
}Scenario Config
interface ScenarioConfig {
id: string; // Unique within endpoint
name: string; // Display name in admin UI
status: number; // HTTP status code (100-599)
body?: any; // Response body (JSON or function) — mutually exclusive with file
file?: string; // File path relative to configPath — mutually exclusive with body
headers?: Record<string, string>; // Custom response headers
delay?: number; // Response delay in ms
}Note:
bodyandfileare mutually exclusive. Setting both will cause a validation error at startup.
Flow Config
Flows activate multiple scenarios at once to simulate complex user journeys.
interface FlowConfig {
id: string; // Unique identifier
name: string; // Display name
description?: string; // Optional description
category?: string; // Optional category for grouping in UI
endpointScenarios: { // Map of endpointId → scenarioId
[endpointId: string]: string;
};
}Example: mock-config/flows/error-flow.json
{
"id": "all-errors",
"name": "All Errors Flow",
"description": "Returns errors for all endpoints",
"category": "Error States",
"endpointScenarios": {
"get-users": "error",
"get-products": "error",
"create-order": "validation-error"
}
}File Download
A scenario can serve a file instead of JSON by specifying a file path relative to configPath:
mock-config/endpoints/reports.json
{
"id": "get-report",
"path": "/reports/:id",
"method": "GET",
"defaultScenarioId": "pdf",
"scenarios": [
{
"id": "pdf",
"name": "PDF Report",
"status": 200,
"file": "./assets/sample-report.pdf",
"headers": { "Content-Disposition": "attachment; filename=report.pdf" }
},
{
"id": "not-found",
"name": "Not Found",
"status": 404,
"body": { "error": "Report not found" }
}
]
}Place the file at mock-config/assets/sample-report.pdf. The Content-Type header is inferred automatically from the file extension if not set explicitly.
bodyandfileare mutually exclusive — setting both causes a validation error at startup.
File Upload
Upload endpoints work out of the box — no special configuration needed. The server accepts multipart/form-data requests, discards the uploaded files, and returns the active scenario response:
mock-config/endpoints/avatar.json
{
"id": "upload-avatar",
"path": "/users/:id/avatar",
"method": "POST",
"defaultScenarioId": "success",
"scenarios": [
{
"id": "success",
"name": "Upload OK",
"status": 200,
"body": { "url": "/cdn/avatar.jpg" }
},
{
"id": "too-large",
"name": "File Too Large",
"status": 413,
"body": { "error": "File exceeds maximum size" }
}
]
}Global Delay
Add a baseline latency to every response without modifying individual scenarios:
const server = new MockServer({
configPath: './mock-config',
globalDelay: 200, // every response waits 200 ms
});Or use a random range to simulate variable network conditions:
const server = new MockServer({
configPath: './mock-config',
globalDelay: [100, 500], // each response waits a random 100–500 ms
});Priority: a scenario's own delay field always takes precedence. globalDelay is only used when the resolved scenario has no delay set.
{
"id": "fast-scenario",
"name": "Always fast",
"status": 200,
"delay": 0,
"body": { "ok": true }
}In the example above, delay: 0 overrides any globalDelay — the response is immediate.
TypeScript Configs
You can write configs in TypeScript for type safety and dynamic responses:
mock-config/endpoints/users.ts
import type { EndpointConfig } from '@zze/mock-server';
const endpoint: EndpointConfig = {
id: 'get-users',
path: '/users',
method: 'GET',
defaultScenarioId: 'success',
scenarios: [
{
id: 'success',
name: 'Success',
status: 200,
body: (req) => {
// Dynamic response based on request
const limit = parseInt(req.query.limit as string) || 10;
return Array.from({ length: limit }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
timestamp: new Date().toISOString()
}));
}
}
]
};
export default endpoint;Admin UI
The built-in admin UI is available at /mock-admin and provides:
- Endpoints Panel: View all endpoints, expand to see scenarios, click to activate
- Flows Panel: View and activate user flows with category grouping
- Create new flows with the endpoint/scenario picker
- Edit or duplicate existing flows
- Delete flows with confirmation
- Collapsible category groups for organization
- Search/filter flows by name
- State Panel: See current active flow and scenario count
- Controls: Refresh data, reset to defaults

Admin REST API
Control the mock server programmatically:
State & Config
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /mock-admin/api/config | Get all endpoints and flows |
| GET | /mock-admin/api/state | Get current active state |
| POST | /mock-admin/api/scenarios/activate | Activate a scenario |
| POST | /mock-admin/api/flows/activate | Activate/deactivate a flow |
| POST | /mock-admin/api/reset | Reset to default state |
Flow CRUD
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /mock-admin/api/flows | Create a new flow |
| PUT | /mock-admin/api/flows/:id | Update an existing flow |
| DELETE | /mock-admin/api/flows/:id | Delete a flow |
| POST | /mock-admin/api/flows/:id/duplicate | Duplicate an existing flow |
Examples
# Get current config
curl http://localhost:3001/mock-admin/api/config
# Activate a scenario
curl -X POST http://localhost:3001/mock-admin/api/scenarios/activate \
-H "Content-Type: application/json" \
-d '{"endpointId": "get-users", "scenarioId": "error"}'
# Activate a flow
curl -X POST http://localhost:3001/mock-admin/api/flows/activate \
-H "Content-Type: application/json" \
-d '{"flowId": "all-errors"}'
# Deactivate flow
curl -X POST http://localhost:3001/mock-admin/api/flows/activate \
-H "Content-Type: application/json" \
-d '{"flowId": null}'
# Reset to defaults
curl -X POST http://localhost:3001/mock-admin/api/reset
# Create a new flow
curl -X POST http://localhost:3001/mock-admin/api/flows \
-H "Content-Type: application/json" \
-d '{"flow": {"id": "my-flow", "name": "My Flow", "category": "Testing", "endpointScenarios": {"get-users": "error"}}}'
# Update a flow
curl -X PUT http://localhost:3001/mock-admin/api/flows/my-flow \
-H "Content-Type: application/json" \
-d '{"flow": {"id": "my-flow", "name": "My Updated Flow", "category": "Testing", "endpointScenarios": {"get-users": "success"}}}'
# Duplicate a flow
curl -X POST http://localhost:3001/mock-admin/api/flows/my-flow/duplicate \
-H "Content-Type: application/json" \
-d '{"newId": "my-flow-copy"}'
# Delete a flow
curl -X DELETE http://localhost:3001/mock-admin/api/flows/my-flowProgrammatic API
MockServer Class
const server = new MockServer({
configPath: './mock-config',
port: 3001, // Default: 3001
apiPrefix: '/api', // Default: '/api'
hotReload: true, // Default: true
globalDelay: 200 // Optional: add 200 ms to every response (unless scenario overrides)
// globalDelay: [100, 500] // Or a random range in ms
});Lifecycle Methods
// Start the server
await server.start();
// Stop the server
await server.stop();
// Restart (stop + start)
await server.restart();
// Manually reload config
await server.reload();State & Info
// Get server state
const state = server.getState();
// {
// running: true,
// port: 3001,
// endpointCount: 5,
// flowCount: 2,
// hotReloadActive: true,
// activeFlowId: null,
// activeScenarios: { 'get-users': 'error' }
// }
// Get loaded endpoints
const endpoints = server.getEndpoints();
// Get loaded flows
const flows = server.getFlows();
// Get config path
const configPath = server.getConfigPath();Control Methods
// Activate a specific scenario
server.activateScenario('get-users', 'error');
// Activate a flow
server.activateFlow('all-errors');
// Deactivate flow
server.activateFlow(null);
// Reset to defaults
server.reset();Event Handlers
// Listen for config reloads
const unsubscribe = server.onReload((event) => {
console.log('Config reloaded:', event.changedFiles);
});
// Listen for errors
server.onError((error) => {
console.error('Server error:', error);
});
// Unsubscribe when done
unsubscribe();Advanced: Access Express App
// Get the Express app instance (for custom middleware)
const app = server.getApp();
if (app) {
app.use('/custom', customMiddleware);
}
// Get the HTTP server (for WebSocket, etc.)
const httpServer = server.getHttpServer();Path Parameters & Patterns
Endpoints support Express-style path patterns:
{
"id": "get-user-by-id",
"path": "/users/:id",
"method": "GET",
"defaultScenarioId": "success",
"scenarios": [
{
"id": "success",
"name": "User Found",
"status": 200,
"body": { "id": 1, "name": "Alice" }
},
{
"id": "not-found",
"name": "Not Found",
"status": 404,
"body": { "error": "User not found" }
}
]
}Dynamic response using path params (TypeScript):
{
id: 'success',
name: 'User Found',
status: 200,
body: (req) => ({
id: parseInt(req.params.id),
name: `User ${req.params.id}`
})
}Response Delays
Simulate network latency:
{
"id": "slow-response",
"name": "Slow Response",
"status": 200,
"body": { "data": "loaded" },
"delay": 2000
}Custom Headers
Add custom response headers:
{
"id": "with-headers",
"name": "With Custom Headers",
"status": 200,
"headers": {
"X-Custom-Header": "custom-value",
"Cache-Control": "no-cache"
},
"body": { "data": "ok" }
}Hot Reload
When hotReload: true (default), the server watches your config directory and automatically reloads when files change:
- Add/remove endpoint files
- Modify scenarios
- Add/remove flows
Changes take effect immediately without restarting the server. A 300ms debounce prevents rapid reloads during saves.
Integration with Tests
Use the mock server in your test setup:
// test/setup.ts
import { MockServer } from '@zze/mock-server';
let server: MockServer;
beforeAll(async () => {
server = new MockServer({
configPath: './test/mock-config',
port: 3099,
hotReload: false
});
await server.start();
});
afterAll(async () => {
await server.stop();
});
beforeEach(() => {
server.reset();
});
// In tests
it('handles error response', async () => {
server.activateScenario('get-users', 'error');
const response = await fetch('http://localhost:3099/api/users');
expect(response.status).toBe(500);
});Troubleshooting
Port already in use
Error: listen EADDRINUSE: address already in use :::3001Change the port or kill the existing process:
lsof -i :3001 | grep LISTEN | awk '{print $2}' | xargs killConfig not loading
- Ensure
configPathpoints to a directory withendpoints/andflows/subdirectories - Check file extensions:
.json,.js, or.ts - Validate JSON syntax
- Check for required fields:
id,path,method,defaultScenarioId,scenarios
TypeScript configs not working
Ensure you have typescript installed. The package uses jiti for zero-config TS loading.
API Reference
Types
// Main options
interface MockServerOptions {
configPath: string;
port?: number;
apiPrefix?: string;
hotReload?: boolean;
watcherOptions?: WatcherOptions;
/**
* Global delay fallback for all responses.
* Scenario-level `delay` always overrides this.
* A number is a fixed ms value; a tuple is a [min, max] random range.
*/
globalDelay?: number | [number, number];
}
// Server state
interface MockServerState {
running: boolean;
port: number | null;
endpointCount: number;
flowCount: number;
hotReloadActive: boolean;
activeFlowId: string | null;
activeScenarios: Record<string, string | null>;
}
// Reload event
interface ReloadEvent {
changedFiles: WatcherEvent[];
timestamp: Date;
}Exports
// Main class
export { MockServer, createMockServer };
// Types
export type {
MockServerOptions,
MockServerState,
EndpointConfig,
ScenarioConfig,
FlowConfig,
HttpMethod,
ResponseBody,
ServerOptions,
ReloadEvent,
WatcherOptions
};
// Constants
export { ADMIN_UI_PATH }; // '/mock-admin'License
MIT
