@molecule/api-mock-server
Utility · mock-server · API (Node) · v1.2.0 · Apache-2.0
Mock API server with deterministic fixture data for testing, screenshots, and E2E
npm install @molecule/api-mock-serverHow it works
@molecule/api-mock-server is a utility package for the API (Node) side (mock-server).
import { createMockServer } from '@molecule/api-mock-server'
const server = await createMockServer({
appType: 'personal-finance',
fixturesPath: './api/fixtures', // directory of *.json fixture files
port: 4000,
})
// Control state programmatically ('GET /accounts' and 'GET /api/accounts'
// are equivalent keys)
server.setState('GET /accounts', { state: 'error', statusCode: 500 })
server.setState('GET /transactions', { state: 'empty' })
server.setDefaultState('empty') // flips every endpoint at once
// Undo an endpoint override so ?_state / the default control it again —
// setState(key, { state: 'success' }) is NOT the same thing (see @remarks)
server.clearState('GET /accounts')
// Teardown
await server.close()Reference
Mock API server with deterministic fixture data for testing, screenshots, and E2E.
Provides a lightweight Express server that serves realistic fixture responses for any molecule app type. Supports per-request state control via query params and headers for testing success, empty, error, and unauthorized states.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { createMockServer } from '@molecule/api-mock-server'
const server = await createMockServer({
appType: 'personal-finance',
fixturesPath: './api/fixtures', // directory of *.json fixture files
port: 4000,
})
// Control state programmatically ('GET /accounts' and 'GET /api/accounts'
// are equivalent keys)
server.setState('GET /accounts', { state: 'error', statusCode: 500 })
server.setState('GET /transactions', { state: 'empty' })
server.setDefaultState('empty') // flips every endpoint at once
// Undo an endpoint override so ?_state / the default control it again —
// setState(key, { state: 'success' }) is NOT the same thing (see @remarks)
server.clearState('GET /accounts')
// Teardown
await server.close()
Type
utility
Installation
npm install @molecule/api-mock-server express zod
npm install -D @types/express
API
Interfaces
RoutableFixture
The minimal shape the router needs from an endpoint fixture. Both a full
EndpointFixture and a JSON-serialized one (see SerializedFixtureSet)
satisfy it.
interface RoutableFixture {
/** The endpoint this fixture answers. */
endpoint: { method: HttpMethod; path: string }
/** Body served in the `success` state. */
successResponse: unknown
/** Body served in the `empty` state. */
emptyResponse: unknown
/** Body served in the `error` state. */
errorResponse: { error: string }
}
SerializedFixtureSet
A fixture set flattened to plain JSON — what a static build bundles.
endpoints keeps the server's key → fixture pairs in registration order.
interface SerializedFixtureSet {
/** App type label the set was built for. */
appType: string
/** `[key, fixture]` pairs, e.g. `['GET /api/posts', { … }]`. */
endpoints: Array<[string, RoutableFixture]>
}
FixtureRoute
One compiled route.
interface FixtureRoute<F extends RoutableFixture = RoutableFixture> {
/** The fixture-set key this route came from (e.g. `GET /api/posts/:id`). */
key: string
/** Upper-case HTTP method. */
method: HttpMethod
/** The route path, always under `/api`. */
path: string
/** Compiled path matcher. */
pattern: RegExp
/** `:param` names in path order. */
paramNames: string[]
/** The fixture that answers this route. */
fixture: F
}
FixtureRouteMatch
A successful match.
interface FixtureRouteMatch<F extends RoutableFixture = RoutableFixture> {
/** The matched route. */
route: FixtureRoute<F>
/** Decoded path parameters. */
params: Record<string, string>
}
FixtureResponse
A status + body pair; body === null means "no body" (e.g. 204).
interface FixtureResponse {
/** HTTP status code. */
status: number
/** JSON body, or `null` for an empty response. */
body: unknown
}
ResponseHint
Hints about the response shape extracted from handler analysis
interface ResponseHint {
/** Whether the response is an array (list endpoint) */
isList: boolean
/** Whether the response is paginated ({ data, total, page, limit }) */
isPaginated: boolean
/** Whether the response includes nested resources */
hasNestedResources: boolean
/** Resource name (e.g. 'accounts', 'transactions') */
resourceName: string
/**
* Whether the success response is a single bare object literal
* (`res.json({ a, b, c })`) rather than a list or `{ data: [] }`
* envelope — e.g. `/analytics/summary`, `/profile/me`. When true,
* `responseFields` holds its top-level keys.
*/
isSingleObject?: boolean
/** Top-level field names of the success `res.json({...})` object, if extracted */
responseFields?: string[]
}
ZodSchemaDefinition
Serialized Zod schema definition for fixture generation
interface ZodSchemaDefinition {
/** Schema type name (e.g. 'ZodObject', 'ZodString') */
type: string
/** Schema shape for objects: field name -> nested schema def */
shape?: Record<string, ZodSchemaDefinition>
/** Enum values if applicable */
enumValues?: string[]
/** Default value if specified */
defaultValue?: unknown
/** Inner type for optional/nullable wrappers */
innerType?: ZodSchemaDefinition
/** Element type for arrays */
elementType?: ZodSchemaDefinition
/** Constraints (min, max, positive, int, etc.) */
constraints?: {
min?: number
max?: number
positive?: boolean
int?: boolean
minLength?: number
maxLength?: number
}
}
EndpointDefinition
A single discovered endpoint from handler scanning
interface EndpointDefinition {
/** HTTP method (GET, POST, PUT, DELETE) */
method: HttpMethod
/** Full route path (e.g. '/accounts', '/accounts/:id', '/reports/net-worth') */
path: string
/** Zod schema for request body (if any) */
bodySchema?: ZodSchemaDefinition
/** Whether the endpoint requires authentication */
requiresAuth: boolean
/** Expected response shape hints */
responseHints: ResponseHint
}
HandlerScanResult
Result of scanning all handlers for an app type
interface HandlerScanResult {
/** App type (e.g. 'personal-finance', 'online-store') */
appType: string
/** All discovered endpoints */
endpoints: EndpointDefinition[]
/** Resource names discovered (accounts, transactions, etc.) */
resources: string[]
}
FixtureConfig
Configuration for fixture generation
interface FixtureConfig {
/** Number of items to generate for list endpoints */
listCount?: number
/** Seed override (default: derived from app type + path) */
seed?: number
/** App type for domain-specific data pools */
appType?: string
}
SemanticRule
A semantic rule for generating realistic values based on field names
interface SemanticRule {
/** Field name pattern (regex) */
pattern: RegExp
/** Generator function that produces a realistic value */
generate: (rng: () => number, index: number) => unknown
}
EndpointFixture
Fixture data for a single endpoint
interface EndpointFixture {
/** The endpoint definition */
endpoint: EndpointDefinition
/** Success response body */
successResponse: unknown
/** Empty state response body */
emptyResponse: unknown
/** Error response */
errorResponse: { error: string }
}
AppFixtureSet
Fixture set for an entire app type
interface AppFixtureSet {
/** App type name */
appType: string
/** Endpoint key -> fixture data */
endpoints: Map<string, EndpointFixture>
}
ResponseState
Response state for controlling mock behavior
interface ResponseState {
/** The state of the response */
state: 'success' | 'empty' | 'error' | 'unauthorized'
/**
* Additional delay in ms before responding. Clamped to `MAX_MOCK_DELAY_MS`
* (60s, see `applyDelay`) — an oversized value is capped and logged
* rather than honored verbatim, so it cannot hang a request indefinitely.
*/
delay?: number
/** Custom status code override */
statusCode?: number
}
MockServerConfig
Configuration for the mock HTTP server
interface MockServerConfig {
/** App type to serve fixtures for (used for display and handler resolution) */
appType: string
/** Path to a directory of JSON fixture files (takes priority over appType for data) */
fixturesPath?: string
/** Port to listen on (default 4000) */
port?: number
/**
* Network address to bind (default `'127.0.0.1'` — loopback ONLY). Pass an
* explicit host (e.g. `'0.0.0.0'`) to opt into LAN-wide exposure; the
* server serves fixture data with permissive CORS, so binding beyond
* loopback is a deliberate act, never a default.
*/
host?: string
/** Default response delay in ms (default 0) */
defaultDelay?: number
/** Default state for all endpoints */
defaultState?: 'success' | 'empty' | 'error' | 'unauthorized'
/** Per-endpoint state overrides (key: "METHOD /path") */
endpointStates?: Record<string, ResponseState>
/** Path to handler templates directory (auto-detected from appType) */
handlersPath?: string
/** Custom fixture data to use instead of auto-generated */
customFixtures?: AppFixtureSet
/** Whether to log requests (default true) */
logging?: boolean
}
MockServer
Running mock server instance with control methods
interface MockServer {
/** The port the server is listening on */
port: number
/** The network address the server bound (default `'127.0.0.1'`) */
host: string
/** The app type being served */
appType: string
/**
* Set a persistent state override for one endpoint (key: `"METHOD /path"`,
* e.g. `'GET /accounts'` — the bare and `/api/`-prefixed forms are
* equivalent keys). This is the HIGHEST-priority source of state for that
* endpoint. Full per-request precedence (highest first):
*
* 1. `setState(endpointKey, ...)` (this method)
* 2. per-request `?_state` query param / `X-Mock-State` header
* 3. `setDefaultState(...)` / the server's configured `defaultState`
*
* So a forgotten `setState('GET /accounts', { state: 'error' })` left over
* from an earlier test SILENTLY beats every later `?_state=success` on
* that endpoint — the request looks like `?_state` is being ignored.
* **Calling `setState(key, { state: 'success' })` does NOT remove the
* override** — it replaces it with an override that happens to look like
* the default, but per-request `?_state`/`X-Mock-State` still can't reach
* that endpoint (the override still outranks them). Use
* `MockServer.clearState` to actually remove the override and hand
* control back to per-request/default state. `setDefaultState()` never
* clears endpoint overrides either — the default is only the fallback used
* when no endpoint override exists at all.
* @param endpointKey - `"METHOD /path"`, e.g. `'GET /accounts'`.
* @param state - The state this endpoint returns for every request until
* `setState` is called again, or `clearState`d, for the same key.
*/
setState: (endpointKey: string, state: ResponseState) => void
/**
* Remove a persistent per-endpoint override previously set with
* `MockServer.setState`, restoring per-request `?_state`/
* `X-Mock-State` (and ultimately `setDefaultState()`) control over that
* endpoint. Pass the EXACT same key string used in the matching
* `setState()` call — keys are matched as opaque strings, not normalized,
* so `setState('GET /accounts', ...)` and `setState('GET /api/accounts',
* ...)` are independent overrides and each needs its own `clearState()`.
* Clearing a key with no active override is a harmless no-op.
* @param endpointKey - The same `"METHOD /path"` key passed to `setState`.
*/
clearState: (endpointKey: string) => void
/**
* Set the fallback state used for any endpoint that has no per-endpoint
* `setState()` override — see `MockServer.setState` for the full
* precedence chain. A per-request `?_state`/`X-Mock-State` value still
* wins over this default; only an active `setState()` override outranks
* both.
* @param state - The fallback response state for endpoints with no override.
*/
setDefaultState: (state: 'success' | 'empty' | 'error' | 'unauthorized') => void
/** Get the fixture set being served */
getFixtures: () => AppFixtureSet
/** Close the server */
close: () => Promise<void>
}
FixtureRecord
Pre-built fixture record for a specific resource
interface FixtureRecord {
[key: string]: unknown
}
AppDataPool
App-specific fixture data pools
interface AppDataPool {
/** App type name */
appType: string
/** Resource-keyed fixture data: resource name -> array of records */
resources: Record<string, FixtureRecord[]>
/** Report endpoints: endpoint path suffix -> response data */
reports?: Record<string, unknown>
}
Types
HttpMethod
HTTP methods supported by the mock server
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
Functions
loadFixturesFromDirectory(fixturesDir, appType)
Load an AppDataPool from a directory of JSON fixture files.
File naming conventions:
- Array files (e.g.
products.jsoncontaining[...]) become CRUD resources - Object files (e.g.
reports.jsoncontaining{key: ...}) become sub-endpoint groups where each key maps to a GET endpoint
Special filenames are treated as report/sub-endpoint groups (object shape expected):
reports.json, storefront.json, admin.json
All other files are treated as array resources by default.
function loadFixturesFromDirectory(fixturesDir: string, appType: string): AppDataPool | undefined
fixturesDir— Absolute path to the fixtures directoryappType— App type label (for the returned pool metadata)
Returns: An AppDataPool, or undefined if the directory doesn't exist or is empty
getAppDataPool(fixturesDir, appType)
Get an AppDataPool for a fixtures directory, with fingerprint-validated
caching: editing/adding/removing a *.json fixture file (mtime or size
change) invalidates the cached pool, so a mock server re-created in the
same process serves the fresh data instead of the first load.
function getAppDataPool(fixturesDir: string, appType: string): AppDataPool | undefined
fixturesDir— Absolute path to the fixtures directoryappType— App type label
Returns: The loaded pool, or undefined if directory missing/empty
buildFixtureSet(appType, endpoints, fixturesDir)
Build a complete fixture set for an app type by combining data pool records with endpoint definitions.
function buildFixtureSet(
appType: string,
endpoints: EndpointDefinition[],
fixturesDir?: string,
): AppFixtureSet | undefined
appType— The app typeendpoints— The discovered endpoints from scanningfixturesDir— Path to the fixtures directory
Returns: A complete fixture set, or undefined if no data available
generateFixtures(fixturesDir, appType)
Generate fixtures from a directory of JSON files without scanning handlers. Builds standard CRUD endpoints for each resource file and custom endpoints for each sub-endpoint group.
function generateFixtures(fixturesDir: string, appType?: string): AppFixtureSet | undefined
fixturesDir— Absolute path to the fixtures directoryappType— App type label (default: derived from directory name)
Returns: A fixture set with standard CRUD endpoints, or undefined if no data
createSeededRandom(seed)
Create a seeded random number generator using Mulberry32. Returns a function that produces numbers in [0, 1).
function createSeededRandom(seed: number): () => number
seed— The seed value
Returns: A function that returns the next pseudo-random number
seedFromPath(appType, path)
Derive a deterministic seed from an app type and endpoint path. The same app+path always produces the same seed, ensuring stable fixture output.
function seedFromPath(appType: string, path: string): number
appType— The application type (e.g. 'personal-finance')path— The endpoint path (e.g. '/accounts')
Returns: A numeric seed value
pick(rng, arr)
Pick a random element from an array using the RNG.
function pick(rng: () => number, arr: readonly T[]): T
rng— The seeded random functionarr— The array to pick from
Returns: A random element from the array
randomInt(rng, min, max)
Pick a random integer in [min, max] inclusive.
function randomInt(rng: () => number, min: number, max: number): number
rng— The seeded random functionmin— Minimum value (inclusive)max— Maximum value (inclusive)
Returns: A random integer in the given range
randomDollars(rng, min, max)
Generate a random dollar amount rounded to cents.
function randomDollars(rng: () => number, min: number, max: number): number
rng— The seeded random functionmin— Minimum valuemax— Maximum value
Returns: A number rounded to 2 decimal places
recentDate(rng, daysBack)
Generate a "recent" ISO date string within the last N days of the fixed
FIXTURE_NOW anchor (NOT the wall clock — see FIXTURE_NOW).
function recentDate(rng: () => number, daysBack?: number): string
rng— The seeded random functiondaysBack— Maximum days in the past (default 90)
Returns: An ISO date string
seededUUID(rng)
Generate a UUID-like string from the RNG (not cryptographically secure).
function seededUUID(rng: () => number): string
rng— The seeded random function
Returns: A UUID v4-like string
futureDate(rng, daysAhead)
Generate a "future" ISO date string within the next N days of the fixed
FIXTURE_NOW anchor (NOT the wall clock — see FIXTURE_NOW).
function futureDate(rng: () => number, daysAhead?: number): string
rng— The seeded random functiondaysAhead— Maximum days in the future (default 365)
Returns: An ISO date string
applySemanticRules(fieldName, rng, index, rules)
Apply semantic rules to generate a value for a given field name.
function applySemanticRules(
fieldName: string,
rng: () => number,
index: number,
rules?: SemanticRule[],
): unknown
fieldName— The name of the field to generate a value forrng— The seeded random functionindex— The item index (for cycling through pools)rules— Custom rules to apply (defaults to defaultSemanticRules)
Returns: The generated value, or undefined if no rule matched
walkSchema(schema, fieldName, rng, index)
Walk a serialized Zod schema definition and produce conformant data.
function walkSchema(
schema: ZodSchemaDefinition,
fieldName: string,
rng: () => number,
index: number,
): unknown
schema— The serialized schema definitionfieldName— The field name (for semantic heuristics)rng— The seeded random functionindex— The item index for list generation
Returns: A conformant value matching the schema
generateRecord(schema, rng, index)
Generate a single conformant record from a schema, enriched with standard fields (id, created_at, updated_at).
function generateRecord(
schema: ZodSchemaDefinition | undefined,
rng: () => number,
index: number,
): Record<string, unknown>
schema— The serialized Zod schema definitionrng— The seeded random functionindex— The item index
Returns: A record with all schema fields plus standard fields
generateRecords(schema, count, appType, path, config)
Generate multiple records from a schema definition.
function generateRecords(
schema: ZodSchemaDefinition | undefined,
count: number,
appType: string,
path: string,
config?: FixtureConfig,
): Record<string, unknown>[]
schema— The serialized Zod schema definitioncount— Number of records to generateappType— App type for seed derivationpath— Endpoint path for seed derivationconfig— Optional fixture configuration
Returns: An array of conformant records
toApiPath(path)
Prefix a route path with /api unless it already starts with it — the
convention the server has always used for scanner-discovered paths.
function toApiPath(path: string): string
path— A route path such as/posts/:idor/api/posts/:id.
Returns: The path under /api.
paramCount(path)
Number of :param segments in a route path.
function paramCount(path: string): number
path— The route path (e.g./profile/:id).
Returns: The parameter count.
compileRoutePath(path)
Compile an Express-style route path into a case-insensitive matcher.
Supports :name parameters, *name wildcards (one or more segments) and
{…} optional groups.
function compileRoutePath(path: string): { pattern: RegExp; paramNames: string[] }
path— The route path.
Returns: The pattern and the parameter names in order.
compileFixtureRoutes(entries)
Compile fixture-set entries into routes, sorted static-before-parameterized (stable, so insertion order is kept within one parameter count).
function compileFixtureRoutes(entries: Iterable<[string, F]>): FixtureRoute<F>[]
entries—[key, fixture]pairs (aMapor an array).
Returns: The compiled routes in match order.
matchFixtureRoute(routes, method, pathname)
Find the route that answers a request.
function matchFixtureRoute(
routes: FixtureRoute<F>[],
method: string,
pathname: string,
): FixtureRouteMatch<F> | undefined
routes— Routes fromcompileFixtureRoutes.method— The request method (any case).pathname— The request path, without query string.
Returns: The match, or undefined when no route answers.
fixtureResponse(fixture, method, state)
The status + body a fixture serves in a given state.
function fixtureResponse(
fixture: RoutableFixture,
method: HttpMethod,
state: ResponseState,
): FixtureResponse
fixture— The matched fixture.method— The request method.state— The effective response state.
Returns: The response; body is null when there is no body to send.
unmatchedFixtureResponse(method, state)
The response for an /api/* request no fixture answers: an empty SUCCESS
([] for GET, 204 for DELETE, {} otherwise) so pages still render — or
the error / unauthorized body when that state was asked for.
function unmatchedFixtureResponse(method: string, state: ResponseState | undefined): FixtureResponse
method— The request method.state— The effective response state, if any.
Returns: The response; body is null for the 204.
serializeFixtureSet(set)
Flatten a fixture set to plain JSON for bundling into a static build. Each endpoint keeps only what the router reads (method, path and the three response bodies), in the set's own order.
function serializeFixtureSet(set: {
appType: string
endpoints: Map<string, RoutableFixture>
}): SerializedFixtureSet
set— A fixture set, e.g. frombuildMockFixtureSet.set.appType— App type label.set.endpoints— Key → fixture map.
Returns: The JSON-safe set; JSON.stringify it into the bundle.
scanHandlers(handlersPath, appType)
Scan all handler files for a given app type and produce endpoint definitions.
function scanHandlers(handlersPath: string, appType: string): HandlerScanResult
handlersPath— Path to the handlers directoryappType— The app type name
Returns: The scan result with all discovered endpoints
resolveHandlersPath(appType, workspaceRoot)
Resolve the handlers path for a given app type. Searches standard locations in the mlcl templates directory.
function resolveHandlersPath(appType: string, workspaceRoot?: string): string | undefined
appType— The app type nameworkspaceRoot— The workspace root directory
Returns: The resolved handlers path, or undefined if not found
stateControlMiddleware(defaultState)
Express middleware that extracts state control signals from the request and attaches them to res.locals for the route handler to use.
function stateControlMiddleware(
defaultState?: ResponseState | (() => ResponseState),
): (req: Request, res: Response, next: NextFunction) => void
defaultState— The default state to use when no override is provided. Pass a function to have the default resolved per-request (a live getter) — required forMockServer.setDefaultState()to take effect after startup, since a plain object is captured once at middleware-creation time.
Returns: Express middleware function
corsMiddleware()
CORS middleware that sets permissive CORS headers for development.
function corsMiddleware(): (req: Request, res: Response, next: NextFunction) => void
Returns: Express middleware function
loggingMiddleware()
Request logging middleware for the mock server.
function loggingMiddleware(): (req: Request, res: Response, next: NextFunction) => void
Returns: Express middleware function
applyDelay(state)
Apply a delay if specified in the response state. The requested delay is
capped at MAX_MOCK_DELAY_MS — a value above the cap is clamped and
a warning is logged (via console.warn, immediately, before waiting —
not after — so the clamp is visible in server logs right when the
oversized delay is requested rather than a minute later).
function applyDelay(state: ResponseState): Promise<void>
state— The response state that may contain a delay
Returns: A promise that resolves after the (possibly clamped) delay, or immediately if no delay was requested
createMockServer(config)
Create and start a mock API server for the given app type. The server discovers endpoints by scanning handler templates and serves deterministic fixture data for each discovered route.
function createMockServer(config: MockServerConfig): Promise<MockServer>
config— Server configuration
Returns: A running MockServer instance with control methods
buildMockFixtureSet(config)
Build the complete fixture set a mock server serves for a config — the
directory fixtures (or customFixtures), enriched with endpoints the
handler scanner discovers that no fixture file covers. Node-only (it reads
the filesystem). createMockServer serves exactly this set; a static build
serializes it with serializeFixtureSet and answers fetch from it
in the browser through the same router.
function buildMockFixtureSet(
config: Pick<
MockServerConfig,
'appType' | 'fixturesPath' | 'handlersPath' | 'customFixtures' | 'logging'
>,
): AppFixtureSet
config— The fixture-related fields of a server config.
Returns: The fixture set.
getStatusCode(state, method)
Get the HTTP status code for a given state and method.
function getStatusCode(state: ResponseState, method: HttpMethod): number
state— The response statemethod— The HTTP method
Returns: The appropriate HTTP status code
getResponseBody(state, method, fixture)
Get the response body for a given state, using the endpoint fixture data.
function getResponseBody(
state: ResponseState,
method: HttpMethod,
fixture: { successResponse: unknown; emptyResponse: unknown; errorResponse: { error: string } },
): unknown
state— The response statemethod— The HTTP methodfixture— The fixture data containing success, empty, and error responses
Returns: The response body, or null for 204 responses
parseState(stateStr)
Parse a state string into a ResponseState object.
function parseState(stateStr: string): ResponseState
stateStr— The state string (e.g. 'success', 'error', 'empty', 'unauthorized')
Returns: The parsed ResponseState. An unrecognized string falls back to DEFAULT_STATES.success — the same forgiving behavior as the per-request ?_state middleware (which additionally labels the response with an X-Mock-Invalid-State header so typos are detectable).
Constants
FIXTURE_NOW
Fixed "now" anchor (2026-04-01T12:00:00Z) for all generated dates.
Dates are deliberately anchored to a constant instead of the wall clock so
fixture output is byte-stable across runs (reliable screenshot diffs).
Consequence: "recent"/"future" are relative to this anchor, not the real
current time — never assert generated dates against Date.now().
const FIXTURE_NOW: Date
defaultSemanticRules
Default semantic rules that match field names to realistic generators. Rules are checked in order; the first match wins.
const defaultSemanticRules: SemanticRule[]
MAX_MOCK_DELAY_MS
Maximum delay, in ms, that applyDelay will actually wait — a
requested delay above this is clamped (and logged) rather than honored
verbatim. Guards against a stray oversized ?_delay / X-Mock-Delay /
defaultDelay / setState({ delay }) value (e.g. a units mistake
applying *1000 twice) hanging a request until the CLIENT gives up —
which in an E2E harness presents as an inexplicable page timeout rather
than an obvious mock misconfiguration.
const MAX_MOCK_DELAY_MS: 60000
DEFAULT_STATES
Default response states for each scenario
const DEFAULT_STATES: {
readonly success: { readonly state: 'success' }
readonly empty: { readonly state: 'empty' }
readonly error: { readonly state: 'error'; readonly statusCode: 500 }
readonly unauthorized: { readonly state: 'unauthorized'; readonly statusCode: 401 }
}
Injection Notes
Requirements
Peer dependencies:
zod>=4.0.0
Runtime Dependencies
expresszod
The server BINDS LOOPBACK ONLY by default (127.0.0.1). It serves fixture
data with permissive CORS and runtime state/delay controls, so a
LAN-wide default bind would expose both to every network peer. To listen
on other interfaces — e.g. inside a container or VM where the host
forwards traffic in — pass an explicit host: '0.0.0.0' in the config
(CLI: --host 0.0.0.0). That is a deliberate opt-in, never the default.
The server uses deterministic seeded PRNG for stable fixture data, making
screenshot comparisons reliable. Fixture data comes from the JSON files in
fixturesPath (array files become CRUD resources; reports/storefront/
admin object files become sub-endpoint groups).
Omitting fixturesPath makes the server resolve
mlcl/templates/apps/<appType>/api/fixtures/ by walking up from process.cwd()
— that only works inside the molecule workspace. In a scaffolded project,
always pass fixturesPath.
Requests to /api/* paths with no matching fixture endpoint return an empty
success (200 [] for GET) so pages still render — the response carries an
X-Mock-Unmatched: true header so a typo'd endpoint can be told apart from
an endpoint that legitimately returned empty data. Similarly, an invalid
?_state/X-Mock-State value is ignored (the default state is served) but
labeled with an X-Mock-Invalid-State response header, so a typo'd state
control is detectable instead of silently looking like "state applied".
State precedence, per request, highest first: (1) an endpoint-level
server.setState(key, state) override, (2) a per-request ?_state query
param / X-Mock-State header, (3) server.setDefaultState(...) / the
configured defaultState. A setState() override is PERSISTENT — a
forgotten override from an earlier test silently beats every later
?_state on that same endpoint. Calling setState(key, { state: 'success' }) again does NOT remove the override (it replaces it with one that looks
like the default, still outranking ?_state); call server.clearState(key)
to actually remove it and hand control back to per-request/default state.
setDefaultState() only changes the fallback and never clears endpoint
overrides.
Response delay (defaultDelay, ?_delay/X-Mock-Delay, or a delay in
setState/setDefaultState) is capped at MAX_MOCK_DELAY_MS (60s) — an
oversized value (e.g. a units mistake applying *1000 twice) is clamped
and logged with console.warn instead of hanging the request until the
client gives up, which in an E2E harness reads as an inexplicable page
timeout rather than a mock misconfiguration.
Static builds (no server). buildMockFixtureSet(config) returns the exact
fixture set a server would serve (directory fixtures + scanner-discovered
endpoints); serializeFixtureSet flattens it to JSON. In the browser,
@molecule/api-mock-server/browser answers fetch from that JSON —
installFixtureFetch({ fixtures }) replaces window.fetch before the app
boots — through the SAME router the server dispatches with
(@molecule/api-mock-server/router), plus an in-memory write overlay (POST
adds to the list, PUT/PATCH merge, DELETE removes, a detail GET finds the
record by id; a reload starts over) and a demo session (signed in as the
first users record, or a sample user; any login succeeds, logout works).
Import ONLY those two subpaths from browser code — this root entry is
server-only and throws when bundled for a browser.