npm.io
1.1.2 • Published 3d ago

@x12i/api-simulator

Licence
MIT
Version
1.1.2
Deps
0
Size
257 kB
Vulns
0
Weekly
0

@x12i/api-simulator

Core TypeScript framework for building simulated API services.

Each endpoint defines exactly one behavior:

  1. fixed — always return the configured response.
  2. relative — render a configured JSON response using values from the request and API-local data.
  3. simulation — run a function that receives the matched request and returns a response.

The core is transport-agnostic. It can be called directly from tests, wrapped by Fastify/Express, or exposed through the included Node.js HTTP adapter.

Data and metadata: this package does not load fixtures from disk, MongoDB, Memorix, or HTTP. Consumers pass an in-memory SimulatorDefinition (api.data + endpoints). Optional helpers (createStore, OpenAPI) still take data you supply. Product-owned package simulators ship their own logic, data, and (when needed) Memorix fixtures — see Package simulators.

New here?

Install

npm install @x12i/api-simulator

Minimal example

import { createApiSimulator } from '@x12i/api-simulator';

const simulator = createApiSimulator({
  apis: [
    {
      id: 'objects-api',
      basePath: '/api/v1',
      data: {
        children: [
          {
            id: 'child-1',
            parentId: '{{request.params.parentId}}'
          },
          {
            id: 'child-2',
            parentId: '{{request.params.parentId}}'
          }
        ]
      },
      endpoints: [
        {
          id: 'health',
          method: 'GET',
          path: '/health',
          behavior: {
            type: 'fixed',
            response: {
              status: 200,
              body: { status: 'ok' }
            }
          }
        },
        {
          id: 'list-children',
          method: 'GET',
          path: '/parents/:parentId/children',
          behavior: {
            type: 'relative',
            response: {
              body: {
                parentId: '{{request.params.parentId}}',
                items: '{{data.children}}'
              }
            }
          }
        },
        {
          id: 'calculate-score',
          method: 'POST',
          path: '/score',
          behavior: {
            type: 'simulation',
            handler: ({ request }) => {
              const values = (request.body as { values: number[] }).values;
              return {
                body: {
                  score: values.reduce((sum, value) => sum + value, 0)
                }
              };
            }
          }
        }
      ]
    }
  ]
});

Dispatch directly

const result = await simulator.dispatch({
  method: 'GET',
  path: '/api/v1/parents/parent-42/children'
});

console.log(result.response.body);

dispatch() throws EndpointNotFoundError when nothing matches. tryDispatch() returns undefined instead.

The three endpoint behaviors

1. Fixed

Use fixed behavior for health endpoints, stable metadata, constants, and simple fixtures.

{
  id: 'capabilities',
  method: 'GET',
  path: '/capabilities',
  behavior: {
    type: 'fixed',
    response: {
      status: 200,
      headers: { 'x-simulator': 'true' },
      body: { features: ['read', 'search'] }
    }
  }
}
2. Relative

Relative behavior starts with configured JSON, then resolves tokens against the matched request and the API's internal data object.

Supported token roots:

  • {{request.params.parentId}}
  • {{request.query.page}}
  • {{request.headers.x-tenant-id}}
  • {{request.body.customer.id}}
  • {{data.children}}
  • Array indexes, such as {{data.users[0].id}}
{
  id: 'children',
  method: 'GET',
  path: '/parents/:parentId/children',
  behavior: {
    type: 'relative',
    response: {
      body: {
        parentId: '{{request.params.parentId}}',
        items: '{{data.children}}'
      }
    }
  }
}

A token occupying the entire string preserves its original type. Therefore, {{request.body.limit}} can produce a number and {{data.children}} can produce an array. A token embedded inside text is converted to a string:

{ message: 'Children for parent {{request.params.parentId}}' }

Tokens inside API-local data are rendered too. This lets a reusable internal fixture inherit an identifier from the incoming request:

data: {
  children: [
    { id: 'child-1', parentId: '{{request.params.parentId}}' }
  ]
}

Relative behavior is strict by default. A missing token throws TemplateResolutionError. Set strict: false to leave unresolved tokens unchanged.

3. Simulation function

Use a simulation function when output needs conditions, calculations, validation, generated values, branching, or error responses.

{
  id: 'quote',
  method: 'POST',
  path: '/quote',
  behavior: {
    type: 'simulation',
    handler: async ({ request, data, apiId, endpointId }) => {
      const quantity = Number((request.body as { quantity: number }).quantity);
      const unitPrice = Number(data.unitPrice);

      return {
        status: 201,
        body: {
          apiId,
          endpointId,
          quantity,
          unitPrice,
          total: quantity * unitPrice
        }
      };
    }
  }
}

Expose as a Node.js service

import { createServer } from 'node:http';
import { createNodeHttpHandler } from '@x12i/api-simulator/node';

createServer(createNodeHttpHandler(simulator)).listen(5520);

The adapter:

  • parses URL path and query values;
  • normalizes request headers to lowercase keys (values may be string | string[]);
  • parses JSON, urlencoded, and text bodies by default; returns Buffer for binary;
  • returns 404 when no simulated endpoint matches;
  • returns 500 when rendering or simulation fails;
  • limits request bodies to 1 MiB by default;
  • writes RawResponseBody verbatim (HTML/XML/CSV/binary).

Multiple APIs

A single simulator can expose many APIs. Each API has its own id, optional basePath, internal data, and endpoints.

const simulator = createApiSimulator({
  apis: [crmApi, billingApi, identityApi]
});

Duplicate METHOD + full path routes are rejected during startup. Structurally ambiguous routes such as /users/:id and /users/:name are also rejected. Static routes are matched before parameter routes, so /users/me wins over /users/:id.

Endpoint contract

type EndpointDefinition = {
  id: string;
  method: HttpMethod;
  path: string;
  enabled?: boolean;
  delayMs?: number;
  behavior:
    | { type: 'fixed'; response: SimulatorResponse }
    | { type: 'relative'; response: SimulatorResponse; strict?: boolean }
    | { type: 'simulation'; handler: SimulationFunction };
};

The discriminated behavior union is the core guarantee: an endpoint cannot validly have two behaviors, and it cannot omit its behavior.

Artificial latency

{
  id: 'slow-search',
  method: 'GET',
  path: '/search',
  delayMs: 750,
  behavior: {
    type: 'fixed',
    response: { body: { items: [] } }
  }
}
my-provider-simulator/
├─ mocks/                    # optional — shared with static-memorix when used
│  ├─ data/
│  ├─ metadata/
│  └─ metadata-packs/
├─ src/
│  ├─ data/                  # seeds for api.data / createStore
│  ├─ simulations/           # package-specific handlers
│  ├─ api.ts                 # createApiSimulator({ apis: [...] })
│  ├─ memorix.ts             # optional — buildServer({ MOCKS_DIR })
│  └─ server.ts              # Node/HTTP entry (tryDispatch ± forward)
└─ package.json

Keep provider-specific data and simulation logic outside this package. @x12i/api-simulator remains the reusable engine.

Package simulators + static-memorix

A package simulator is a product-owned package that depends on @x12i/api-simulator and ships:

  1. Logicsimulation handlers (and optional @x12i/api-simulator/store state)
  2. Dataapi.data seeds and/or JSON fixtures
  3. Metadata — when the product uses Memorix, packs and explorer fixtures

For local/CI parity with Memorix Explorer and Metadata APIs, compose @x12i/static-memorix against a shared mocks/ directory:

Surface Owner
/api/v1/<your-product>/* @x12i/api-simulator endpoints in your package
/api/explorer/*, /api/metadata/* @x12i/static-memorix (MOCKS_DIR → shared mocks/)

Pattern: load fixture JSON into createStore / api.data for package REST; set MOCKS_DIR and buildServer() for explorer/metadata; optionally one HTTP port that tryDispatchs package routes and forwards misses to static-memorix.

node examples/flowstate-simulator/demo.mjs
node examples/flowstate-simulator/src/server.mjs   # :5522

static-memorix is not a substitute for full memorix-service (pipelines, relationship materialize, abstract reverse-write, etc.).

Development

npm install
npm run validate

validate runs type checking, tests, and the production build.

Playground and Studio
npm run build
npm run dev -w @x12i/playground   # http://127.0.0.1:5523
npm run dev -w @x12i/studio       # http://127.0.0.1:5521

See docs/playground-studio.md.

Default listen ports come from @x12i/ports-manager zone api-simulator (5520–5539, even = API, odd = UI). Wrapper: scripts/api-simulator-ports.mjs.

Name Port Role
api / library-demo / codegen 5520 API
studio (Vite) 5521 UI
flowstate compose server 5522 API
playground (Vite) 5523 UI
npx x12i-ports zone api-simulator

Raw (non-JSON) responses

import { RawResponseBody, createApiSimulator } from '@x12i/api-simulator';

// In a simulation handler:
return {
  body: new RawResponseBody('<h1>hello</h1>', 'text/html; charset=utf-8')
};

Transport adapters write RawResponseBody verbatim instead of JSON.stringify.

In-memory store

import { createStore } from '@x12i/api-simulator/store';

const users = createStore([{ id: '1', name: 'Ada' }]);
users.create({ name: 'Grace' });
users.list({ name: 'Ada' });
users.reset(); // restore seed — useful between tests

Auth / rate-limit helpers

import { requireBearerToken, rateLimit } from '@x12i/api-simulator/helpers';

handler: rateLimit({ windowMs: 60_000, max: 30 })(
  requireBearerToken((token) => token === 'secret')(
    ({ request }) => ({ body: { ok: true, path: request.path } })
  )
)

Simulator options

const simulator = createApiSimulator(definition, {
  recordHistory: { limit: 100 },
  validateTemplates: true // checks {{data.*}} at construction
});

simulator.getHistory();
simulator.clearHistory();

OpenAPI skeletons

import { openapiToApiDefinition } from '@x12i/api-simulator/openapi';

const api = openapiToApiDefinition(parsedOpenApiObject, { id: 'pets-api', basePath: '/v1' });

Pass an already-parsed JS object (parse YAML yourself if needed).

Fetch adapter

import { createFetchHandler } from '@x12i/api-simulator/fetch';

export default {
  fetch: createFetchHandler(simulator)
};
Express / Fastify recipes
// Express
app.use(async (req, res) => {
  const match = await simulator.tryDispatch({
    method: req.method,
    path: req.path,
    headers: req.headers,
    query: req.query,
    body: req.body
  });
  if (!match) return res.status(404).json({ error: 'NOT_FOUND' });
  res.status(match.response.status ?? 200).set(match.response.headers ?? {}).send(match.response.body);
});

CORS, host routing, validators

{
  id: 'api',
  host: 'tenant.example.com',
  cors: { origins: '*', methods: ['GET', 'POST', 'OPTIONS'] },
  endpoints: [{
    id: 'create',
    method: 'POST',
    path: '/items',
    validate: ({ request }) => {
      if (!request.body) return { status: 400, body: { error: 'EMPTY' } };
    },
    behavior: { type: 'fixed', response: { status: 201, body: { ok: true } } }
  }]
}

Record and replay

node scripts/record-replay.mjs --base https://httpbin.org --paths /get,/uuid --out captured.json

Node body parsers

Built-in parsers: JSON, application/x-www-form-urlencoded, text/plain. Binary bodies are returned as Buffer. Plug in multipart yourself:

createNodeHttpHandler(simulator, {
  bodyParsers: {
    'multipart/form-data': async (raw, contentType) => {
      // use busboy / your parser of choice
      return { rawLength: raw.length, contentType };
    }
  }
});

Path patterns

Supports :id, :id(\\d+), and optional :slug? segments.

Release notes

1.1.2
1.1.1
  • Default local ports moved into an org port band (superseded by 1.1.2 / ports-manager).
1.1.0
  • REST gap-closure: createStore, helpers, OpenAPI skeletons, fetch adapter, CORS/host/validators, raw bodies, record-replay script, Playground/Studio apps.
  • Package-simulator docs and examples/flowstate-simulator: compose @x12i/static-memorix with shared mocks and a tutorial that connects logic, data, and metadata.

Keywords