Create the example
Use macOS with Node.js 22.23 or later. This example needs no Docker, MCP registration, or application packages. Install the CLI:
npm install -g previewhostFor other installation choices, see Installation. Create a new directory outside an existing Git repository:
mkdir previewhost-demo
cd previewhost-demoSave this as backend.mjs:
import { createServer } from 'node:http';
createServer((request, response) => {
if (request.url !== '/message') { response.writeHead(404).end(); return; }
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ message: 'Hello from the backend.' }));
}).listen(Number(process.env.PORT), process.env.HOST);Save this as frontend.mjs:
import { createServer } from 'node:http';
const page = `<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Previewhost demo</title>
<h1>Frontend + backend</h1>
<p id="message" role="status">Connecting…</p>
<script type="module">
const message = document.querySelector('#message');
try {
const response = await fetch('/message');
if (!response.ok) throw new Error('Backend unavailable');
message.textContent = (await response.json()).message;
} catch {
message.textContent = 'Backend request failed.';
}
</script>`;
createServer(async (request, response) => {
if (request.url === '/message') {
try {
const reply = await fetch(new URL('/message', process.env.BACKEND_URL), {
signal: AbortSignal.timeout(2000),
});
response.writeHead(reply.status, {
'content-type': 'application/json', 'cache-control': 'no-store',
});
response.end(await reply.text());
} catch {
response.writeHead(503).end('Backend unavailable');
}
} else if (request.url === '/') {
response.writeHead(200, { 'content-type': 'text/html' });
response.end(page);
} else response.writeHead(404).end();
}).listen(Number(process.env.PORT), process.env.HOST);The frontend forwards /message to the backend. Browser requests use the frontend's origin, so this example needs no CORS configuration.
Describe the services
Save this as preview.yaml beside the two server files:
name: hello
type: environment
primary: frontend
services:
backend:
type: command
cwd: .
command: [node, backend.mjs]
readyPath: /message
frontend:
type: command
cwd: .
command: [node, frontend.mjs]
readyPath: /message
env:
BACKEND_URL: {service: backend}primary selects the frontend for the environment URL.
The service binding supplies the backend URL and starts the frontend after the backend is ready.
Both servers use Previewhost's PORT and HOST values.
Start and inspect
Check the configuration:
previewhost inspectStart the preview:
previewhost start --allow-exec--allow-exec permits these commands to run with your user permissions. It provides no sandbox.
The CLI finds or starts a persistent background owner for this directory.
Open the url from the JSON result. The page shows Frontend + backend, then Hello from the backend.
Readiness checks HTTP headers. The page check also covers the request from the frontend to the backend.
If the result says starting, wait for the returned attempt ID:
previewhost wait hello ATTEMPT_IDReplace ATTEMPT_ID with the ID from the result. A wait timeout does not cancel startup.
To inspect services and logs in the browser, run previewhost dashboard in another terminal.
Try a replacement
Change the backend message in backend.mjs. Then run:
previewhost replaceReload the same URL after the replacement is ready. The new message appears. These example servers do not reload source automatically. Development servers can have their own reload behavior.
Stop the example
previewhost stop hello
previewhost shutdownStop ends the preview. Shutdown ends this project's owner and all its previews. The source files remain. For a project with databases, stop also retains managed data.
Next, write preview.yaml for your own application.