view-api 1.1.1 → 3.0.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Ridlo Achmad Ghifary
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -6,10 +6,11 @@ Perfect for frontend development, testing, and prototyping without a real backen
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - Run mock APIs from a single JSON file
9
+ - Run a command and get editor and the api fetchable
10
10
  - Auto Refresh JSON file
11
11
  - Customizable port
12
12
  - Randomized success / error responses
13
+ - Delay response
13
14
  - Zero setup for frontend teams
14
15
  - Works fully offline
15
16
 
@@ -20,62 +21,96 @@ You don’t need to install it globally.
20
21
  Run directly with `npx`:
21
22
 
22
23
  ```bash
23
- npx view-api start ./mock.json --port 4000
24
+ # simpler way
25
+ npx view-api dev
26
+
27
+ or
28
+
29
+ # use your own file
30
+ npx view-api dev <file-path>
24
31
  ```
25
32
 
26
33
  Or install globally:
27
34
 
28
35
  ```bash
29
36
  npm install -g view-api
30
- view-api start mock.json
37
+ view-api dev
31
38
  ```
32
39
 
33
40
  ## 🚀 Usage
34
41
 
35
42
  ```bash
36
- view-api start <config-path> [options]
43
+ view-api dev <config-path> [options]
37
44
  ```
38
45
 
39
46
  ### Options
40
47
 
41
- | Option | Description | Default |
42
- | -------------- | ---------------------- | ------- |
43
- | `--port`, `-p` | Port to run the server | `3000` |
48
+ | Option | Description | Default |
49
+ | ------------ | ---------------------- | ------- |
50
+ | `--api-port` | Port to run the api | `8723` |
51
+ | `--ui-port` | Port to run the editor | `8724` |
44
52
 
45
53
  Example:
46
54
 
47
55
  ```bash
48
- view-api start src/mocks/mock.json --port 4000
56
+ view-api dev src/mocks/mock.json --api-port 4000 --ui-port 4001
57
+ ```
58
+
59
+ Then you will have running editor and the API endpoint:
60
+
61
+ ```bash
62
+ ➜ API running at http://localhost:4000
63
+ ➜ EDITOR running at http://localhost:4001
49
64
  ```
50
65
 
51
66
  ## 📄 Mock Config Format
52
67
 
53
68
  ```json
54
69
  {
55
- "version": "1.0.0",
56
- "routes": {
57
- "GET /products": {
58
- "behavior": {
59
- "successRate": 70
70
+ "GET /products": {
71
+ "behavior": {
72
+ "successRate": 50
73
+ },
74
+ "responses": {
75
+ "success": {
76
+ "statusCode": 200,
77
+ "body": {
78
+ "status": "success",
79
+ "message": "Products fetched wkwk",
80
+ "data": [
81
+ {
82
+ "id": 1,
83
+ "name": "Product A",
84
+ "price": 10000,
85
+ "stock": 50
86
+ },
87
+ {
88
+ "id": 2,
89
+ "name": "Product B",
90
+ "price": 15000,
91
+ "stock": 30
92
+ }
93
+ ]
94
+ }
60
95
  },
61
- "responses": {
62
- "success": {
63
- "statusCode": 200,
96
+ "errors": [
97
+ {
98
+ "statusCode": 500,
64
99
  "body": {
65
- "status": "success",
66
- "data": [{ "id": 1, "name": "Product A" }]
100
+ "status": "failed",
101
+ "message": "Server error",
102
+ "error_code": "SERVER_ERROR"
67
103
  }
68
104
  },
69
- "errors": [
70
- {
71
- "statusCode": 500,
72
- "body": {
73
- "status": "failed",
74
- "message": "Server error"
75
- }
105
+ {
106
+ "statusCode": 400,
107
+ "body": {
108
+ "status": "failed",
109
+ "message": "Bad request, invalid parameters",
110
+ "error_code": "INVALID_PARAMETERS"
76
111
  }
77
- ]
78
- }
112
+ }
113
+ ]
79
114
  }
80
115
  }
81
116
  }
@@ -83,4 +118,4 @@ view-api start src/mocks/mock.json --port 4000
83
118
 
84
119
  ## 📜 License
85
120
 
86
- MIT
121
+ Licensed under the MIT License.
@@ -1,20 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import path from "path";
4
3
  import { program } from "commander";
5
- import startServer from "../src/server.js";
4
+ import { startApiServer } from "../src/api/server.js";
5
+ import { startEditorServer } from "../src/editor/server.js";
6
+ import { loadConfig } from "../src/shared/config.js";
6
7
 
7
8
  program
8
- .name("mock-runner")
9
- .description("Run mock APIs from a JSON config")
10
- .version("1.0.0");
9
+ .name("view-api")
10
+ .description("Run mock APIs locally from a JSON configuration file")
11
+ .version("2.0.0");
11
12
 
13
+ /**
14
+ * DEV — API + Editor
15
+ */
12
16
  program
13
- .command("start <config>")
14
- .option("-p, --port <port>", "port to run server", "3000")
15
- .action((config, options) => {
16
- const configPath = path.resolve(process.cwd(), config);
17
- startServer({ configPath, port: Number(options.port) || 8734 });
17
+ .command("dev [configPath]")
18
+ .description("Start mock API with live editor UI")
19
+ .option("--api-port <port>", "API port", 8723)
20
+ .option("--ui-port <port>", "Editor UI port", 8724)
21
+ .action((configPath, options) => {
22
+ if (configPath && !configPath.endsWith(".json")) {
23
+ console.error("➜ Config file must be a .json file");
24
+ process.exit(1);
25
+ }
26
+
27
+ if (options.apiPort === options.uiPort) {
28
+ console.error("➜ API port and UI port must be different");
29
+ process.exit(1);
30
+ }
31
+
32
+ loadConfig(configPath);
33
+
34
+ startApiServer({ port: Number(options.apiPort) });
35
+ startEditorServer({ port: Number(options.uiPort) });
18
36
  });
19
37
 
20
38
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "view-api",
3
- "version": "1.1.1",
3
+ "version": "3.0.0",
4
4
  "description": "Run mock APIs locally from a JSON configuration file",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "scripts": {
23
- "dev": "nodemon bin/mock-runner.js start src/mocks/mock.json",
23
+ "dev": "nodemon bin/mock-runner.js dev src/mocks/mock.json",
24
24
  "start": "node bin/mock-runner.js",
25
25
  "prepublishOnly": "npm run lint || true"
26
26
  },
@@ -31,16 +31,22 @@
31
31
  "cli",
32
32
  "frontend",
33
33
  "testing",
34
- "json"
34
+ "json",
35
+ "view-api"
35
36
  ],
36
- "author": "Ridlo",
37
+ "author": "Ridlo Achmad Ghifary <ridloghfry@gmail.com>",
38
+ "private": false,
37
39
  "license": "MIT",
38
40
  "engines": {
39
41
  "node": ">=18"
40
42
  },
41
43
  "dependencies": {
44
+ "@codemirror/lang-json": "^6.0.2",
45
+ "@codemirror/theme-one-dark": "^6.1.3",
46
+ "codemirror": "^6.0.2",
42
47
  "commander": "^14.0.2",
43
48
  "cors": "^2.8.5",
49
+ "dotenv": "^17.2.3",
44
50
  "express": "^5.2.1",
45
51
  "helmet": "^8.1.0",
46
52
  "morgan": "^1.10.1"
@@ -0,0 +1,42 @@
1
+ import { getConfig } from "../shared/config.js";
2
+ import { chance, pickRandom } from "./random.js";
3
+
4
+ export const handleMockRequest = (req, res) => {
5
+ const key = `${req.method.toUpperCase()} ${req.path}`;
6
+ const config = getConfig()[key];
7
+
8
+ if (!config) {
9
+ return res.status(404).json({
10
+ status: "failed",
11
+ message: "Mock not found",
12
+ });
13
+ }
14
+
15
+ const isSuccess = chance(config?.behavior?.successRate ?? 100);
16
+ const errors = config?.responses?.errors ?? [];
17
+
18
+ if (isSuccess || !errors?.length) {
19
+ const { statusCode = 200, body } = config.responses.success;
20
+ const delay = config?.behavior?.delay ?? 0;
21
+
22
+ if (!body) {
23
+ return res.status(statusCode).json("No response body defined");
24
+ }
25
+
26
+ if (delay > 0) {
27
+ return setTimeout(() => {
28
+ res.status(statusCode).json(body);
29
+ }, delay);
30
+ }
31
+
32
+ return res.status(statusCode).json(body);
33
+ }
34
+
35
+ const error = pickRandom(errors);
36
+
37
+ if (!error.body) {
38
+ return res.status(error.statusCode ?? 500).json("No error body defined");
39
+ }
40
+
41
+ return res.status(error.statusCode ?? 500).json(error.body);
42
+ };
@@ -0,0 +1,7 @@
1
+ export const chance = (percent) => {
2
+ return Math.random() * 100 < percent;
3
+ };
4
+
5
+ export const pickRandom = (arr) => {
6
+ return arr[Math.floor(Math.random() * arr.length)];
7
+ };
@@ -0,0 +1,20 @@
1
+ import express from "express";
2
+ import morgan from "morgan";
3
+ import cors from "cors";
4
+ import helmet from "helmet";
5
+ import { handleMockRequest } from "./handler.js";
6
+
7
+ export const startApiServer = ({ port }) => {
8
+ const app = express();
9
+
10
+ app.use(express.json());
11
+ app.use(cors());
12
+ app.use(helmet());
13
+ app.use(morgan("dev"));
14
+
15
+ app.use(handleMockRequest);
16
+
17
+ app.listen(port, () =>
18
+ console.log(`➜ API running at http://localhost:${port}`),
19
+ );
20
+ };
@@ -0,0 +1,78 @@
1
+ import { json } from "https://esm.sh/@codemirror/lang-json";
2
+ import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark";
3
+ import { EditorView, basicSetup } from "https://esm.sh/codemirror";
4
+
5
+ let editor, saveTimer, statusTimer;
6
+ let lastValid = true;
7
+
8
+ const statusEl =
9
+ document.getElementById("status") || document.createElement("div");
10
+ statusEl.id = "status";
11
+ document.body.appendChild(statusEl);
12
+
13
+ // load config
14
+ const res = await fetch("/__config");
15
+ const data = await res.json();
16
+
17
+ const customTheme = EditorView.theme({
18
+ "&": {
19
+ fontSize: "14px",
20
+ fontFamily: `"JetBrains Mono", "Fira Code", monospace`,
21
+ },
22
+ ".cm-content": {
23
+ padding: "16px",
24
+ },
25
+ });
26
+
27
+ editor = new EditorView({
28
+ doc: JSON.stringify(data, null, 2),
29
+ extensions: [
30
+ basicSetup,
31
+ json(),
32
+ oneDark,
33
+ customTheme,
34
+ EditorView.updateListener.of((update) => {
35
+ if (!update.docChanged) return;
36
+ debounceSave();
37
+ }),
38
+ ],
39
+ parent: document.getElementById("editor"),
40
+ });
41
+
42
+ function debounceSave() {
43
+ clearTimeout(saveTimer);
44
+ saveTimer = setTimeout(saveConfig, 700);
45
+ }
46
+
47
+ async function saveConfig() {
48
+ try {
49
+ const value = editor.state.doc.toString();
50
+ if (!value.trim().endsWith("}")) return;
51
+
52
+ const parsed = JSON.parse(value);
53
+
54
+ await fetch("/__config", {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify(parsed),
58
+ });
59
+
60
+ showStatus("Saved ✓", false);
61
+ lastValid = true;
62
+ } catch (err) {
63
+ showStatus("Invalid JSON ✗", true);
64
+ lastValid = false;
65
+ }
66
+ }
67
+
68
+ function showStatus(text, isError) {
69
+ clearTimeout(statusTimer);
70
+
71
+ statusEl.textContent = text;
72
+ statusEl.className = isError ? "error" : "ok";
73
+ statusEl.style.opacity = "1";
74
+
75
+ statusTimer = setTimeout(() => {
76
+ statusEl.style.opacity = "0";
77
+ }, 2500);
78
+ }
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>View API Editor</title>
6
+ <link rel="stylesheet" href="./style.css" />
7
+ </head>
8
+ <body>
9
+ <div id="editor"></div>
10
+ <div id="status"></div>
11
+
12
+ <script type="module" src="./editor.js"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,40 @@
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ padding: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ }
8
+
9
+ #editor {
10
+ height: 100vh;
11
+ width: 100vw;
12
+ }
13
+
14
+ /* CodeMirror full height fix */
15
+ .cm-editor {
16
+ height: 100%;
17
+ }
18
+
19
+ #status {
20
+ position: fixed;
21
+ bottom: 0;
22
+ left: 0;
23
+ right: 0;
24
+ padding: 6px 12px;
25
+ font-size: 12px;
26
+ text-align: center;
27
+ transition: opacity 0.3s ease;
28
+ opacity: 0;
29
+ pointer-events: none;
30
+ }
31
+
32
+ #status.ok {
33
+ background: #064e3b;
34
+ color: #a7f3d0;
35
+ }
36
+
37
+ #status.error {
38
+ background: #7f1d1d;
39
+ color: #fecaca;
40
+ }
@@ -0,0 +1,30 @@
1
+ import express from "express";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { getConfig, setConfig } from "../shared/config.js";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export const startEditorServer = ({ port }) => {
10
+ const app = express();
11
+ app.use(express.json());
12
+
13
+ // serve UI
14
+ app.use(express.static(path.join(__dirname, "public")));
15
+
16
+ // get current config
17
+ app.get("/__config", (_req, res) => {
18
+ res.json(getConfig());
19
+ });
20
+
21
+ // save config
22
+ app.post("/__config", (req, res) => {
23
+ setConfig(req.body);
24
+ res.json({ ok: true });
25
+ });
26
+
27
+ app.listen(port, () =>
28
+ console.log(`➜ EDITOR running at http://localhost:${port}`),
29
+ );
30
+ };
@@ -1,51 +1,48 @@
1
1
  {
2
- "version": "1.0.0",
3
- "routes": {
4
- "GET /products": {
5
- "behavior": {
6
- "successRate": 50
2
+ "GET /products": {
3
+ "behavior": {
4
+ "successRate": 50
5
+ },
6
+ "responses": {
7
+ "success": {
8
+ "statusCode": 200,
9
+ "body": {
10
+ "status": "success",
11
+ "message": "Products fetched wkwk",
12
+ "data": [
13
+ {
14
+ "id": 1,
15
+ "name": "Product A",
16
+ "price": 10000,
17
+ "stock": 50
18
+ },
19
+ {
20
+ "id": 2,
21
+ "name": "Product B",
22
+ "price": 15000,
23
+ "stock": 30
24
+ }
25
+ ]
26
+ }
7
27
  },
8
- "responses": {
9
- "success": {
10
- "statusCode": 200,
28
+ "errors": [
29
+ {
30
+ "statusCode": 500,
11
31
  "body": {
12
- "status": "success",
13
- "message": "Products fetched wkwk",
14
- "data": [
15
- {
16
- "id": 1,
17
- "name": "Product A",
18
- "price": 10000,
19
- "stock": 50
20
- },
21
- {
22
- "id": 2,
23
- "name": "Product B",
24
- "price": 15000,
25
- "stock": 30
26
- }
27
- ]
32
+ "status": "failed",
33
+ "message": "Server error",
34
+ "error_code": "SERVER_ERROR"
28
35
  }
29
36
  },
30
- "errors": [
31
- {
32
- "statusCode": 500,
33
- "body": {
34
- "status": "failed",
35
- "message": "Server error",
36
- "error_code": "SERVER_ERROR"
37
- }
38
- },
39
- {
40
- "statusCode": 400,
41
- "body": {
42
- "status": "failed",
43
- "message": "Bad request, invalid parameters",
44
- "error_code": "INVALID_PARAMETERS"
45
- }
37
+ {
38
+ "statusCode": 400,
39
+ "body": {
40
+ "status": "failed",
41
+ "message": "Bad request, invalid parameters",
42
+ "error_code": "INVALID_PARAMETERS"
46
43
  }
47
- ]
48
- }
44
+ }
45
+ ]
49
46
  }
50
47
  }
51
48
  }
@@ -0,0 +1,23 @@
1
+ import fs from "fs";
2
+ import { defaultMockConfig } from "./defaultConfig.js";
3
+
4
+ let config = {};
5
+
6
+ export const loadConfig = (path) => {
7
+ if (!path) {
8
+ config = structuredClone(defaultMockConfig);
9
+ return;
10
+ }
11
+
12
+ if (!fs.existsSync(path)) {
13
+ throw new Error(`Mock config file not found: ${path}`);
14
+ }
15
+
16
+ config = JSON.parse(fs.readFileSync(path, "utf-8"));
17
+ };
18
+
19
+ export const getConfig = () => config;
20
+
21
+ export const setConfig = (next) => {
22
+ config = next;
23
+ };
@@ -0,0 +1,54 @@
1
+ export const defaultMockConfig = {
2
+ "GET /health": {
3
+ behavior: {
4
+ successRate: 100,
5
+ delay: 3000, // milliseconds
6
+ },
7
+ responses: {
8
+ success: {
9
+ statusCode: 200,
10
+ body: {
11
+ status: "success",
12
+ message: "Life is good!",
13
+ data: { healthy: true },
14
+ },
15
+ },
16
+ errors: [
17
+ {
18
+ statusCode: 500,
19
+ body: {
20
+ status: "error",
21
+ message: "Internal server error",
22
+ },
23
+ },
24
+ ],
25
+ },
26
+ },
27
+
28
+ "GET /products": {
29
+ behavior: {
30
+ successRate: 50,
31
+ },
32
+ responses: {
33
+ success: {
34
+ statusCode: 200,
35
+ body: {
36
+ status: "success",
37
+ data: [
38
+ { id: 1, name: "Product A", price: 120000 },
39
+ { id: 2, name: "Product B", price: 90000 },
40
+ ],
41
+ },
42
+ },
43
+ errors: [
44
+ {
45
+ statusCode: 500,
46
+ body: {
47
+ status: "error",
48
+ message: "Internal server error",
49
+ },
50
+ },
51
+ ],
52
+ },
53
+ },
54
+ };
package/src/app.js DELETED
@@ -1,29 +0,0 @@
1
- import express from "express";
2
- import morgan from "morgan";
3
- import helmet from "helmet";
4
- import cors from "cors";
5
-
6
- // define routes here
7
- import mockRoutes from "./routes/mock.routes.js";
8
-
9
- import "dotenv/config";
10
-
11
- export default function createApp(configPath) {
12
- const app = express();
13
-
14
- // ==== MIDDLEWARES ====
15
- app.use(express.json());
16
- app.use(express.urlencoded({ extended: true }));
17
- app.use(morgan("dev"));
18
- app.use(cors());
19
- app.use(helmet());
20
-
21
- app.use((req, _res, next) => {
22
- req.mockConfigPath = configPath;
23
- next();
24
- });
25
-
26
- app.use(mockRoutes);
27
-
28
- return app;
29
- }
@@ -1,21 +0,0 @@
1
- import { handle } from "../services/mock.service.js";
2
-
3
- export const handleRequest = async (req, res, next) => {
4
- try {
5
- // call service to handle request
6
- const data = handle(req.mockConfigPath, req.method, req.path);
7
- console.log("🚀 ~ handleRequest ~ data:", data);
8
-
9
- if (!data) {
10
- return res.status(404).json({
11
- status: "failed",
12
- message: `No mock defined for ${req.method} ${req.path}`,
13
- });
14
- }
15
-
16
- // return response
17
- return res.status(data.statusCode).json(data.body);
18
- } catch (err) {
19
- next(err);
20
- }
21
- };
@@ -1,8 +0,0 @@
1
- import express from "express";
2
- import { handleRequest } from "../controllers/mock.controller.js";
3
-
4
- const router = express.Router();
5
-
6
- router.use(handleRequest);
7
-
8
- export default router;
package/src/server.js DELETED
@@ -1,10 +0,0 @@
1
- import createApp from "./app.js";
2
-
3
- export default ({ configPath, port }) => {
4
- const server = createApp(configPath);
5
-
6
- server.listen(port, () => {
7
- console.log(` ➜ [API] Server running on: http://localhost:${port}`);
8
- console.log(` ➜ [API] Using config file: ${configPath}`);
9
- });
10
- };
@@ -1,15 +0,0 @@
1
- import { loadConfig } from "../utils/loadConfig.js";
2
- import { pickResponse } from "../utils/responsePicker.js";
3
-
4
- export const handle = (mockConfigPath, method, path) => {
5
- const config = loadConfig(mockConfigPath);
6
- const key = `${method} ${path}`;
7
-
8
- const route = config.routes[key];
9
-
10
- if (!route) {
11
- return null;
12
- }
13
-
14
- return pickResponse(route);
15
- };
@@ -1,6 +0,0 @@
1
- import fs from "fs";
2
-
3
- export const loadConfig = (configPath) => {
4
- const raw = fs.readFileSync(configPath, "utf-8");
5
- return JSON.parse(raw);
6
- };
@@ -1,19 +0,0 @@
1
- export const pickResponse = (routeConfig) => {
2
- const { behavior, responses } = routeConfig;
3
- const roll = Math.random() * 100;
4
-
5
- if (roll <= behavior.successRate) {
6
- return {
7
- statusCode: responses.success.statusCode,
8
- body: responses.success.body,
9
- };
10
- }
11
-
12
- const errors = responses.errors;
13
- const error = errors[Math.floor(Math.random() * errors.length)];
14
-
15
- return {
16
- statusCode: error.statusCode,
17
- body: error.body,
18
- };
19
- };