How to Build a Mock API and Webhook Tester Without Running a Local Tunnel

This app has three launch paths:

  • build a mock API from routes in the UI
  • import an API file and turn it into routes
  • run a webhook receiver or raw server file directly

The outer app handles editing and imports. BrowserPod runs the server inside /project and exposes it through the portal URL.

1. Normalize uploaded files into one route format

Start by converting the formats people already have into one internal routes[] shape. In this demo, parseUploadedFile() accepts OpenAPI 3, Swagger 2, Postman, HAR, Insomnia, routes.json, and raw .js server files:

export function parseUploadedFile(filename, content) {
	const lower = filename.toLowerCase();
	if (lower.endsWith(".js")) {
		return { type: "raw", formatLabel: "Server file" };
	}

	const parsed = yaml.load(content);
	const format = detectFormat(parsed);

	let routes;
	switch (format) {
		case "openapi3":
			routes = openapiToRoutes(parsed);
			break;
		case "swagger2":
			routes = swagger2ToRoutes(parsed);
			break;
		case "postman":
			routes = postmanToRoutes(parsed);
			break;
		case "har":
			routes = harToRoutes(parsed);
			break;
		case "insomnia":
			routes = insomniaToRoutes(parsed);
			break;
		case "routes":
			routes = parsed;
			break;
	}

	return {
		type: format === "routes" ? "routes" : "openapi",
		routes,
		formatLabel,
	};
}

Once everything becomes routes[], the rest of the app only has to deal with one runtime shape.

2. Boot BrowserPod and wait for the portal URL

When the user clicks RUN, boot a pod, create a terminal, and register pod.onPortal(...) before starting the server:

const pod = await BrowserPod.boot({ apiKey: import.meta.env.VITE_BP_APIKEY });
await new Promise((r) => setTimeout(r, 500));

const terminal = await pod.createDefaultTerminal(consoleEl);

pod.onPortal(({ url }) => {
	const routeCount = getRoutes().length;
	serverState = { url, routeCount, isWebhook };
	setStatus(isWebhook ? "Webhook Live" : "API Running", "running");
	portalUrlEl.textContent = url;
	openBtn.href = url;
});

await pod.createDirectory("/project");

The portal callback is the handoff point between the server inside the pod and the UI outside it.

3. Write the runtime into /project

The launch step branches by mode:

  • webhook mode copies the webhook runtime
  • raw mode writes the uploaded server file
  • API mode copies the mock API runtime and writes routes.json
if (isWebhook) {
	await copyFileTo(pod, "webhook/main.js", "/project/main.js");
	await copyFileTo(pod, "webhook/package.json", "/project/package.json");
	await pod.run("npm", ["install"], { echo: true, terminal, cwd: "/project" });
	pod.run("node", ["main.js"], { echo: true, terminal, cwd: "/project" });
} else if (isRaw) {
	await writeTextFile(pod, "/project/raw-server.js", uploadedRawContent);
	await copyFile(pod, "project/package.json");
	pod.run("node", ["raw-server.js"], { echo: true, terminal, cwd: "/project" });
} else {
	await copyFile(pod, "project/main.js");
	await copyFile(pod, "project/package.json");
	await writeTextFile(pod, "/project/routes.json", JSON.stringify(getRoutes()));
	await pod.run("npm", ["install"], { echo: true, terminal, cwd: "/project" });
	pod.run("node", ["main.js"], { echo: true, terminal, cwd: "/project" });
}

Two details matter here:

  • npm install is awaited because the server depends on it
  • node main.js is not awaited because it needs to stay running

4. Serve the mock API from routes.json

The mock API runtime is a small Express server. It reads /project/routes.json, registers one Express handler per route, and returns either JSON, text, or binary content:

var routes = [];
try {
	routes = JSON.parse(fs.readFileSync("/project/routes.json", "utf-8"));
} catch (e) {
	console.error("Could not read routes.json: " + e.message);
}

routes.forEach(function (r) {
	var method = r.method.toLowerCase();
	if (typeof app[method] !== "function") return;

	app[method](r.path, function (req, res) {
		res.status(r.status);
		var ct = r.contentType || "application/json";
		res.set("Content-Type", ct);

		if (typeof r.body === "string" && r.body.startsWith("data:")) {
			var comma = r.body.indexOf(",");
			var b64 = comma !== -1 ? r.body.slice(comma + 1) : r.body;
			res.send(Buffer.from(b64, "base64"));
		} else if (ct === "application/json") {
			res.json(r.body);
		} else {
			res.send(typeof r.body === "string" ? r.body : JSON.stringify(r.body));
		}
	});
});

This demo also adds permissive CORS headers so browser frontends can call the mock API without extra setup.

5. Use a catch-all Express app for webhooks

Webhook mode uses a different runtime. It parses the request body once, stores a short in-memory history, and treats every request except / and /__webhooks as an incoming webhook:

app.use(function (req, res, next) {
	var data = "";
	req.on("data", function (chunk) {
		data += chunk;
	});
	req.on("end", function () {
		req.rawBody = data;
		if (data.trim()) {
			try {
				req.parsedBody = JSON.parse(data);
			} catch (_) {
				req.parsedBody = data;
			}
		} else {
			req.parsedBody = null;
		}
		next();
	});
});

app.use(function (req, res) {
	var entry = {
		id: webhooks.length + 1,
		method: req.method,
		path: req.path,
		headers: headers,
		body: req.parsedBody,
		receivedAt: new Date().toISOString(),
	};

	webhooks.unshift(entry);
	res.end(JSON.stringify({ received: true, id: entry.id }));
});

The embedded dashboard polls GET /__webhooks to show the captured requests. The public URL can receive requests on any path.

6. Allow raw server uploads

If the uploaded file is a .js server, skip route extraction and run it directly:

await writeTextFile(pod, "/project/raw-server.js", uploadedRawContent);
await copyFile(pod, "project/package.json");
pod.run("node", ["raw-server.js"], { echo: true, terminal, cwd: "/project" });

For this path to work cleanly, the uploaded file needs to behave like a server and listen on port 3000.

7. Keep the runtime constraints explicit

This demo is session-bound:

  • closing or refreshing the tab stops the server
  • one tab hosts one running server
  • the mock API and webhook runtimes expect port 3000

BrowserPod setup still applies:

  • BrowserPod.boot(...) needs an API key
  • cross-origin isolation is required
  • production needs HTTPS
  • pure JavaScript dependencies are the safest default
  • native binaries need a WASM-compatible alternative
  • each boot and runtime activity consumes tokens

Ready to get started?

Spin up your first sandbox in minutes. No credit card required.