api-json-server 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # mockserve
2
+
3
+ A mock API server driven by a JSON spec. It is designed for fast development, repeatable mock data, and clear documentation with OpenAPI output and Swagger UI.
4
+
5
+ ## Highlights
6
+
7
+ - JSON spec defines endpoints, responses, and matching rules.
8
+ - Built-in templating for request params, query, and body.
9
+ - Faker-powered data generation with arrays and ranges.
10
+ - Variants with match rules for alternate responses.
11
+ - Error and latency simulation.
12
+ - OpenAPI JSON/YAML plus Swagger UI out of the box.
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ npm install
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ 1) Create a spec file (example `mock.spec.json`):
23
+
24
+ ```
25
+ {
26
+ "version": 1,
27
+ "settings": {
28
+ "delayMs": 0,
29
+ "errorRate": 0,
30
+ "errorStatus": 500,
31
+ "errorResponse": { "error": "Mock error" }
32
+ },
33
+ "endpoints": [
34
+ {
35
+ "method": "GET",
36
+ "path": "/users/:id",
37
+ "response": {
38
+ "id": "{{params.id}}",
39
+ "type": "{{query.type}}"
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ 2) Start the server:
47
+
48
+ ```
49
+ npm run dev -- serve --spec mock.spec.json
50
+ ```
51
+
52
+ 3) Test the endpoint:
53
+
54
+ ```
55
+ curl "http://localhost:3000/users/42?type=basic"
56
+ ```
57
+
58
+ ## CLI
59
+
60
+ ```
61
+ mockserve serve --spec mock.spec.json --port 3000 --watch
62
+ ```
63
+
64
+ Options:
65
+
66
+ - `--spec <path>`: Path to the JSON spec (default `mock.spec.json`).
67
+ - `--port <number>`: Port to run on (default `3000`).
68
+ - `--watch` / `--no-watch`: Reload when spec changes (default: watch enabled).
69
+ - `--base-url <url>`: Base URL used in OpenAPI `servers[]`.
70
+
71
+ ## Spec Reference
72
+
73
+ The spec is validated by `mockserve.spec.schema.json`. It is composed of:
74
+
75
+ ```
76
+ {
77
+ "version": 1,
78
+ "settings": { ... },
79
+ "endpoints": [ ... ]
80
+ }
81
+ ```
82
+
83
+ ### Settings
84
+
85
+ ```
86
+ {
87
+ "delayMs": 0,
88
+ "errorRate": 0,
89
+ "errorStatus": 500,
90
+ "errorResponse": { "error": "Mock error" },
91
+ "fakerSeed": 123
92
+ }
93
+ ```
94
+
95
+ - `delayMs`: Adds artificial latency in milliseconds.
96
+ - `errorRate`: Probability of returning `errorResponse` (0.0 to 1.0).
97
+ - `errorStatus`: HTTP status code for errors.
98
+ - `errorResponse`: Response used when errors are triggered (supports templates).
99
+ - `fakerSeed`: Optional seed for deterministic faker output.
100
+
101
+ ### Endpoints
102
+
103
+ ```
104
+ {
105
+ "method": "GET",
106
+ "path": "/users/:id",
107
+ "match": { "query": { "type": "premium" } },
108
+ "status": 200,
109
+ "response": { ... },
110
+ "delayMs": 0,
111
+ "errorRate": 0,
112
+ "errorStatus": 500,
113
+ "errorResponse": { "error": "Mock error" },
114
+ "variants": [ ... ]
115
+ }
116
+ ```
117
+
118
+ - `method`: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.
119
+ - `path`: Fastify style params (`/users/:id`).
120
+ - `match`: Optional match rules (query and body).
121
+ - `status`: Default status (default `200`).
122
+ - `response`: Response body template.
123
+ - `delayMs`, `errorRate`, `errorStatus`, `errorResponse`: Optional overrides per endpoint.
124
+ - `variants`: Optional array of alternative responses with their own match rules.
125
+
126
+ ### Match Rules
127
+
128
+ ```
129
+ "match": {
130
+ "query": { "type": "premium" },
131
+ "body": { "password": "secret" }
132
+ }
133
+ ```
134
+
135
+ Matching is exact at the top level (strings, numbers, booleans). If a request does not satisfy match rules, the endpoint returns a `404` with `{ "error": "No matching mock for request" }`.
136
+
137
+ ### Variants
138
+
139
+ Variants let you specify alternate responses based on match rules. The first matching variant wins.
140
+
141
+ ```
142
+ "variants": [
143
+ {
144
+ "name": "invalid password",
145
+ "match": { "body": { "password": "wrong" } },
146
+ "status": 401,
147
+ "response": { "ok": false, "error": "Invalid credentials" }
148
+ }
149
+ ]
150
+ ```
151
+
152
+ ### Response Templates
153
+
154
+ Responses support a mix of static values, request placeholders, faker directives, and repeat directives.
155
+
156
+ #### String placeholders
157
+
158
+ - `{{params.id}}`
159
+ - `{{query.type}}`
160
+ - `{{body.email}}`
161
+
162
+ ```
163
+ {
164
+ "id": "{{params.id}}",
165
+ "type": "{{query.type}}",
166
+ "email": "{{body.email}}"
167
+ }
168
+ ```
169
+
170
+ #### Faker directives
171
+
172
+ Use any `@faker-js/faker` method via a dotted path:
173
+
174
+ ```
175
+ { "__faker": "person.firstName" }
176
+ { "__faker": "internet.email" }
177
+ { "__faker": { "method": "string.alpha", "args": [16] } }
178
+ ```
179
+
180
+ #### Repeat directives
181
+
182
+ Repeat directives generate arrays of items:
183
+
184
+ ```
185
+ {
186
+ "__repeat": {
187
+ "min": 10,
188
+ "max": 15,
189
+ "template": { "id": { "__faker": "string.uuid" } }
190
+ }
191
+ }
192
+ ```
193
+
194
+ You can also use a fixed `count`:
195
+
196
+ ```
197
+ {
198
+ "__repeat": {
199
+ "count": 3,
200
+ "template": { "name": { "__faker": "company.name" } }
201
+ }
202
+ }
203
+ ```
204
+
205
+ Notes:
206
+
207
+ - If `count` is provided, it is used as-is.
208
+ - If `min` is omitted, it defaults to `0`.
209
+ - If `max` is missing, `min` is used.
210
+ - If `max < min`, the server returns a `500` error.
211
+
212
+ ## Example: Users List with Faker
213
+
214
+ ```
215
+ {
216
+ "method": "GET",
217
+ "path": "/users",
218
+ "response": {
219
+ "users": {
220
+ "__repeat": {
221
+ "min": 10,
222
+ "max": 15,
223
+ "template": {
224
+ "id": { "__faker": "string.uuid" },
225
+ "firstName": { "__faker": "person.firstName" },
226
+ "lastName": { "__faker": "person.lastName" },
227
+ "avatarUrl": { "__faker": "image.avatar" },
228
+ "phone": { "__faker": "phone.number" },
229
+ "email": { "__faker": "internet.email" },
230
+ "company": { "__faker": "company.name" },
231
+ "joinedAt": { "__faker": { "method": "date.recent", "args": [30] } }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ ## OpenAPI and Swagger UI
240
+
241
+ Endpoints available:
242
+
243
+ - `GET /__openapi.json`
244
+ - `GET /__openapi.yaml`
245
+ - `GET /docs`
246
+ - `GET /__spec`
247
+ - `GET /health`
248
+
249
+ `/docs` serves Swagger UI backed by the generated OpenAPI document.
250
+
251
+ ## Examples Folder
252
+
253
+ See `examples/` for ready-to-use specs:
254
+
255
+ - `examples/basic-crud.json`
256
+ - `examples/auth-variants.json`
257
+ - `examples/users-faker.json`
258
+ - `examples/companies-nested.json`
259
+ - `examples/orders-and-matches.json`
260
+
261
+ ## Programmatic Usage
262
+
263
+ ```
264
+ import { buildServer } from "./dist/server.js";
265
+ import { loadSpecFromFile } from "./dist/loadSpec.js";
266
+
267
+ const spec = await loadSpecFromFile("mock.spec.json");
268
+ const app = buildServer(spec, { specPath: "mock.spec.json", loadedAt: new Date().toISOString() });
269
+ await app.listen({ port: 3000 });
270
+ ```
271
+
272
+ ## Running Tests
273
+
274
+ ```
275
+ npm test
276
+ ```
277
+
278
+ ## Tips
279
+
280
+ - Use `fakerSeed` for deterministic outputs in demos and tests.
281
+ - Combine placeholders with faker for realistic and contextual data.
282
+ - Keep variant rules specific; the first match wins.
283
+
284
+ ## License
285
+
286
+ ISC
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sleep = sleep;
4
+ exports.shouldFail = shouldFail;
5
+ exports.resolveBehavior = resolveBehavior;
6
+ /**
7
+ * Pause for the given number of milliseconds.
8
+ */
9
+ function sleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+ /**
13
+ * Decide whether a request should fail based on an error rate.
14
+ */
15
+ function shouldFail(errorRate) {
16
+ if (errorRate <= 0)
17
+ return false;
18
+ if (errorRate >= 1)
19
+ return true;
20
+ return Math.random() < errorRate;
21
+ }
22
+ /**
23
+ * Resolve behavior settings with precedence: chosen overrides -> endpoint overrides -> global settings.
24
+ */
25
+ function resolveBehavior(settings, endpointOverrides, chosenOverrides) {
26
+ return {
27
+ delayMs: chosenOverrides?.delayMs ?? endpointOverrides?.delayMs ?? settings.delayMs,
28
+ errorRate: chosenOverrides?.errorRate ?? endpointOverrides?.errorRate ?? settings.errorRate,
29
+ errorStatus: chosenOverrides?.errorStatus ?? endpointOverrides?.errorStatus ?? settings.errorStatus,
30
+ errorResponse: chosenOverrides?.errorResponse ?? endpointOverrides?.errorResponse ?? settings.errorResponse
31
+ };
32
+ }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const node_fs_1 = require("node:fs");
5
6
  const program = new commander_1.Command();
6
7
  program
7
8
  .name("mockserve")
@@ -12,10 +13,119 @@ program
12
13
  .description("Start the mock server.")
13
14
  .option("-p, --port <number>", "Port to run the server on", "3000")
14
15
  .option("-s, --spec <path>", "Path to the spec JSON file", "mock.spec.json")
15
- .action((opts) => {
16
- console.log("mockserve serve called with:");
17
- console.log(`- port: ${opts.port}`);
18
- console.log(`- spec: ${opts.spec}`);
19
- console.log("Server not implemented yet.");
16
+ .option("--watch", "Reload when spec file changes", true)
17
+ .option("--no-watch", "Disable reload when spec file changes")
18
+ .option("--base-url <url>", "Public base URL used in OpenAPI servers[] (e.g. https://example.com)")
19
+ .action(async (opts) => {
20
+ await startCommand(opts);
20
21
  });
22
+ /**
23
+ * Run the mock server CLI command.
24
+ */
25
+ async function startCommand(opts) {
26
+ const port = Number(opts.port);
27
+ if (!Number.isFinite(port) || port <= 0) {
28
+ console.error(`Invalid port: ${opts.port}`);
29
+ process.exit(1);
30
+ }
31
+ const specPath = opts.spec;
32
+ const { loadSpecFromFile } = await import("./loadSpec.js");
33
+ const { buildServer } = await import("./server.js");
34
+ let app = null;
35
+ let isReloading = false;
36
+ let debounceTimer = null;
37
+ /**
38
+ * Build and start a server using the current spec file.
39
+ */
40
+ async function startWithSpec() {
41
+ const loadedAt = new Date().toISOString();
42
+ const spec = await loadSpecFromFile(specPath);
43
+ console.log(`Loaded spec v${spec.version} with ${spec.endpoints.length} endpoint(s).`);
44
+ const nextApp = buildServer(spec, { specPath, loadedAt, baseUrl: opts.baseUrl });
45
+ try {
46
+ await nextApp.listen({ port, host: "0.0.0.0" });
47
+ }
48
+ catch (err) {
49
+ nextApp.log.error(err);
50
+ throw err;
51
+ }
52
+ nextApp.log.info(`Mock server running on http://localhost:${port}`);
53
+ nextApp.log.info(`Spec: ${specPath} (loadedAt=${loadedAt})`);
54
+ return nextApp;
55
+ }
56
+ /**
57
+ * Reload the server when the spec changes.
58
+ */
59
+ async function reload() {
60
+ if (isReloading)
61
+ return;
62
+ isReloading = true;
63
+ try {
64
+ console.log("Reloading spec...");
65
+ // 1) Stop accepting requests on the old server FIRST
66
+ if (app) {
67
+ console.log("Closing current server...");
68
+ await app.close();
69
+ console.log("Current server closed.");
70
+ app = null;
71
+ }
72
+ // 2) Start a new server on the same port with the updated spec
73
+ app = await startWithSpec();
74
+ console.log("Reload complete.");
75
+ }
76
+ catch (err) {
77
+ console.error("Reload failed.");
78
+ // At this point the old server may already be closed. We want visibility.
79
+ console.error(String(err));
80
+ // Optional: try to start again to avoid being down
81
+ try {
82
+ if (!app) {
83
+ console.log("Attempting to start server again after reload failure...");
84
+ app = await startWithSpec();
85
+ console.log("Recovery start succeeded.");
86
+ }
87
+ }
88
+ catch (err2) {
89
+ console.error("Recovery start failed. Server is down until next successful reload.");
90
+ console.error(String(err2));
91
+ }
92
+ }
93
+ finally {
94
+ isReloading = false;
95
+ }
96
+ }
97
+ // Initial start
98
+ try {
99
+ app = await startWithSpec();
100
+ }
101
+ catch (err) {
102
+ console.error(String(err));
103
+ process.exit(1);
104
+ }
105
+ // Watch spec for changes
106
+ /**
107
+ * Handle file changes with a debounced reload.
108
+ */
109
+ function onSpecChange() {
110
+ debounceTimer = scheduleReload(reload, debounceTimer);
111
+ }
112
+ if (opts.watch) {
113
+ console.log(`Watching spec file for changes: ${specPath}`);
114
+ // fs.watch emits multiple events; debounce to avoid rapid reload loops
115
+ (0, node_fs_1.watch)(specPath, onSpecChange);
116
+ }
117
+ else {
118
+ console.log("Watch disabled (--no-watch).");
119
+ }
120
+ }
121
+ /**
122
+ * Schedule a debounced reload when the spec changes.
123
+ */
124
+ function scheduleReload(reload, debounceTimer) {
125
+ if (debounceTimer)
126
+ clearTimeout(debounceTimer);
127
+ return setTimeout(() => {
128
+ void reload();
129
+ }, 200);
130
+ }
21
131
  program.parse(process.argv);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadSpecFromFile = loadSpecFromFile;
4
+ const promises_1 = require("node:fs/promises");
5
+ const spec_js_1 = require("./spec.js");
6
+ /**
7
+ * Load and validate a mock spec from disk.
8
+ */
9
+ async function loadSpecFromFile(specPath) {
10
+ let raw;
11
+ try {
12
+ raw = await (0, promises_1.readFile)(specPath, 'utf-8');
13
+ }
14
+ catch (err) {
15
+ throw new Error(`Failed to read spec file ${specPath}: ${err instanceof Error ? err.message : String(err)}`);
16
+ }
17
+ let json;
18
+ try {
19
+ json = JSON.parse(raw);
20
+ }
21
+ catch (err) {
22
+ throw new Error(`Failed to parse spec file ${specPath}: ${err instanceof Error ? err.message : String(err)}`);
23
+ }
24
+ const parsed = spec_js_1.MockSpecSchema.safeParse(json);
25
+ if (!parsed.success) {
26
+ const issues = parsed.error.issues
27
+ .map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`)
28
+ .join("\n");
29
+ throw new Error(`Invalid spec file ${specPath}: ${issues}`);
30
+ }
31
+ return parsed.data;
32
+ }
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateOpenApi = generateOpenApi;
4
+ /**
5
+ * Convert Fastify-style route params to OpenAPI style.
6
+ */
7
+ function toOpenApiPath(fastifyPath) {
8
+ // Fastify style: /users/:id -> OpenAPI style: /users/{id}
9
+ return fastifyPath.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
10
+ }
11
+ /**
12
+ * Extract path parameter names from a Fastify-style route.
13
+ */
14
+ function extractPathParams(fastifyPath) {
15
+ const matches = [...fastifyPath.matchAll(/:([A-Za-z0-9_]+)/g)];
16
+ return matches.map((m) => m[1]);
17
+ }
18
+ /**
19
+ * Deduplicate values while preserving order.
20
+ */
21
+ function uniq(items) {
22
+ return [...new Set(items)];
23
+ }
24
+ /**
25
+ * Convert a list of values into a list of string enums.
26
+ */
27
+ function asStringEnum(values) {
28
+ return uniq(values
29
+ .filter((v) => v !== undefined && v !== null)
30
+ .map((v) => String(v)));
31
+ }
32
+ /**
33
+ * Generate a minimal OpenAPI document for the mock spec.
34
+ */
35
+ function generateOpenApi(spec, serverUrl) {
36
+ const paths = {};
37
+ for (const ep of spec.endpoints) {
38
+ const oasPath = toOpenApiPath(ep.path);
39
+ const method = ep.method.toLowerCase();
40
+ const pathParams = extractPathParams(ep.path);
41
+ // Collect query match keys/values across endpoint + variants
42
+ const queryMatchValues = {};
43
+ const bodyMatchKeys = new Set();
44
+ /**
45
+ * Collect query match values into a set for documentation.
46
+ */
47
+ const collectQuery = (obj) => {
48
+ if (!obj)
49
+ return;
50
+ for (const [k, v] of Object.entries(obj)) {
51
+ if (!queryMatchValues[k])
52
+ queryMatchValues[k] = [];
53
+ queryMatchValues[k].push(String(v));
54
+ }
55
+ };
56
+ /**
57
+ * Collect body match keys for request body documentation.
58
+ */
59
+ const collectBody = (obj) => {
60
+ if (!obj)
61
+ return;
62
+ for (const k of Object.keys(obj))
63
+ bodyMatchKeys.add(k);
64
+ };
65
+ collectQuery(ep.match?.query);
66
+ collectBody(ep.match?.body);
67
+ if (ep.variants?.length) {
68
+ for (const v of ep.variants) {
69
+ collectQuery(v.match?.query);
70
+ collectBody(v.match?.body);
71
+ }
72
+ }
73
+ // Parameters: path params + known query keys
74
+ const parameters = [];
75
+ for (const p of pathParams) {
76
+ parameters.push({
77
+ name: p,
78
+ in: "path",
79
+ required: true,
80
+ schema: { type: "string" }
81
+ });
82
+ }
83
+ for (const [k, vals] of Object.entries(queryMatchValues)) {
84
+ const enumVals = asStringEnum(vals);
85
+ parameters.push({
86
+ name: k,
87
+ in: "query",
88
+ required: false,
89
+ schema: enumVals.length > 0 ? { type: "string", enum: enumVals } : { type: "string" },
90
+ description: "Query param used by mock matching (if configured)."
91
+ });
92
+ }
93
+ // Request body: for non-GET/DELETE, document as generic object with known keys (from match rules)
94
+ const hasRequestBody = ep.method !== "GET" && ep.method !== "DELETE";
95
+ const requestBody = hasRequestBody && bodyMatchKeys.size > 0
96
+ ? {
97
+ required: false,
98
+ content: {
99
+ "application/json": {
100
+ schema: {
101
+ type: "object",
102
+ properties: Object.fromEntries([...bodyMatchKeys].map((k) => [k, { type: "string" }]))
103
+ }
104
+ }
105
+ }
106
+ }
107
+ : undefined;
108
+ // Responses: base + variants (grouped per status)
109
+ const responses = {};
110
+ /**
111
+ * Add a response example to the OpenAPI response map.
112
+ */
113
+ const addResponseExample = (status, name, example) => {
114
+ const key = String(status);
115
+ if (!responses[key]) {
116
+ responses[key] = {
117
+ description: "Mock response",
118
+ content: { "application/json": { examples: {} } }
119
+ };
120
+ }
121
+ const examples = responses[key].content["application/json"].examples;
122
+ examples[name] = { value: example };
123
+ };
124
+ // Base response
125
+ addResponseExample(ep.status ?? 200, "default", ep.response);
126
+ // Variant responses
127
+ if (ep.variants?.length) {
128
+ for (const v of ep.variants) {
129
+ addResponseExample(v.status ?? ep.status ?? 200, v.name ?? "variant", v.response);
130
+ }
131
+ }
132
+ const operation = {
133
+ summary: `Mock ${ep.method} ${ep.path}`,
134
+ parameters: parameters.length ? parameters : undefined,
135
+ requestBody,
136
+ responses
137
+ };
138
+ if (!paths[oasPath])
139
+ paths[oasPath] = {};
140
+ paths[oasPath][method] = operation;
141
+ }
142
+ return {
143
+ openapi: "3.0.3",
144
+ info: {
145
+ title: "mockserve",
146
+ version: "0.1.0",
147
+ description: "OpenAPI document generated from mockserve JSON spec."
148
+ },
149
+ servers: [{ url: serverUrl }],
150
+ paths
151
+ };
152
+ }