In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.
In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.
In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.
Bước 1: Scaffold endpoint preview Step 1: Scaffold the Preview Endpoint ជំហាន 1: Scaffold endpoint preview
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- A `/preview` endpoint that clones a repository into the sandbox.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- gitCheckout() is the quickest way to get a full project into the sandbox for preview or testing.
Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:
Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:
Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:
if (url.pathname === "/preview" && request.method === "POST") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
// Clone if not already present
const { exists: alreadyCloned } = await previewSandbox.exists(
"/workspace/vite-react",
);
if (!alreadyCloned) {
await previewSandbox.gitCheckout(
"https://github.com/harshil1712/vite-react.git",
);
}
return Response.json({
steps: {
clone: {
status: "success",
message: alreadyCloned ? "Already cloned" : "Repository cloned",
},
},
});
} Test it:
Test it:
Test it:
curl -X POST http://localhost:8787/preview {
"steps": {
"clone": { "status": "success", "message": "Repository cloned" }
}
} The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.
The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.
The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.
FROM docker.io/cloudflare/sandbox:0.7.0
RUN git clone https://github.com/harshil1712/vite-react.git /workspace/vite-react
RUN cd /workspace/vite-react && npm install
EXPOSE 8080 Bước 2: Cài dependencies Step 2: Install Dependencies ជំហាន 2: ដំឡើង dependencies
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- Dependencies installed in the sandbox so the application can run.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- Nearly every Node.js project requires npm install before it can start — this is no different inside a sandbox.
Add an install step to the same endpoint, right after the clone logic:
Add an install step to the same endpoint, right after the clone logic:
Add an install step to the same endpoint, right after the clone logic:
if (url.pathname === "/preview" && request.method === "POST") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
// 1. Clone if needed
const { exists: alreadyCloned } = await previewSandbox.exists(
"/workspace/vite-react",
);
if (!alreadyCloned) {
await previewSandbox.gitCheckout(
"https://github.com/harshil1712/vite-react.git",
);
}
// 2. Install dependencies
const install = await previewSandbox.exec("npm install", {
cwd: "/workspace/vite-react",
timeout: 120000, // 2 minutes — npm install can be slow
});
return Response.json({
steps: {
clone: {
status: "success",
message: alreadyCloned ? "Already cloned" : "Repository cloned",
},
install: {
status: install.exitCode === 0 ? "success" : "failed",
message:
install.exitCode === 0 ? "Dependencies installed" : install.stderr,
},
},
});
} Test again — same endpoint, more information:
Test again — same endpoint, more information:
Test again — same endpoint, more information:
curl -X POST http://localhost:8787/preview {
"steps": {
"clone": { "status": "success", "message": "Already cloned" },
"install": { "status": "success", "message": "Dependencies installed" }
}
} Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.
Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.
Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.
Bước 3: Start dev server Step 3: Start the Development Server ជំហាន 3: Start dev server
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- A running Vite dev server inside the sandbox, started as a background process.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- startProcess() runs long-lived processes without blocking your Worker — the process keeps running after the HTTP response is sent.
The key difference from exec():
The key difference from exec():
The key difference from exec():
- exec() — waits for the command to finish, then returns the result
- startProcess() — starts the command and returns immediately with a process handle
Add the server start after install:
Add the server start after install:
Add the server start after install:
if (url.pathname === "/preview" && request.method === "POST") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
// 1. Clone if needed
const { exists: alreadyCloned } = await previewSandbox.exists(
"/workspace/vite-react",
);
if (!alreadyCloned) {
await previewSandbox.gitCheckout(
"https://github.com/harshil1712/vite-react.git",
);
}
// 2. Install dependencies
const install = await previewSandbox.exec("npm install", {
cwd: "/workspace/vite-react",
timeout: 120000,
});
// 3. Start the dev server (non-blocking)
const server = await previewSandbox.startProcess(
"npm run dev -- --port 8080",
{ cwd: "/workspace/vite-react" },
);
// Wait until the server is actually listening
await server.waitForPort(8080, { timeout: 30000 });
return Response.json({
steps: {
clone: {
status: "success",
message: alreadyCloned ? "Already cloned" : "Repository cloned",
},
install: {
status: install.exitCode === 0 ? "success" : "failed",
message:
install.exitCode === 0 ? "Dependencies installed" : install.stderr,
},
server: {
status: "success",
message: "Dev server running",
processId: server.id,
},
},
});
} curl -X POST http://localhost:8787/preview {
"steps": {
"clone": { "status": "success", "message": "Already cloned" },
"install": { "status": "success", "message": "Dependencies installed" },
"server": {
"status": "success",
"message": "Dev server running",
"processId": "proc_abc123"
}
}
} Bước 4: Expose port và proxy request Step 4: Expose Port and Proxy Requests ជំហាន 4: Expose port និង proxy request
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- Your sandbox app accessible via your Worker's URL, with all requests proxied through.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- exposePort() and proxyToSandbox() turn your container port into a real URL users can visit.
Two things need to happen before the preview URL works:
Two things need to happen before the preview URL works:
Two things need to happen before the preview URL works:
First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:
First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:
First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:
FROM docker.io/cloudflare/sandbox:0.7.0
# Required: expose every port you plan to use
EXPOSE 8080 Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:
Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:
Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";
// Required: re-export the Sandbox Durable Object class
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
// Check for proxied requests first — must be before all other routes
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
const url = new URL(request.url);
// ... rest of your routes
},
}; Now add exposePort() as the final stage in the /preview endpoint:
Now add exposePort() as the final stage in the /preview endpoint:
Now add exposePort() as the final stage in the /preview endpoint:
if (url.pathname === "/preview" && request.method === "POST") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
// 1. Clone if needed
const { exists: alreadyCloned } = await previewSandbox.exists(
"/workspace/vite-react",
);
if (!alreadyCloned) {
await previewSandbox.gitCheckout(
"https://github.com/harshil1712/vite-react.git",
);
}
// 2. Install dependencies
const install = await previewSandbox.exec("npm install", {
cwd: "/workspace/vite-react",
timeout: 120000,
});
// 3. Start the dev server
const server = await previewSandbox.startProcess(
"npm run dev -- --port 8080",
{ cwd: "/workspace/vite-react" },
);
await server.waitForPort(8080, { timeout: 30000 });
// 4. Expose the port — returns a URL users can visit
const hostname = new URL(request.url).host; // For production, use hostname instead of host
const exposed = await previewSandbox.exposePort(8080, { hostname });
return Response.json({
steps: {
clone: {
status: "success",
message: alreadyCloned ? "Already cloned" : "Repository cloned",
},
install: {
status: install.exitCode === 0 ? "success" : "failed",
message:
install.exitCode === 0 ? "Dependencies installed" : install.stderr,
},
server: {
status: "success",
message: "Dev server running",
processId: server.id,
},
expose: {
status: "success",
message: "Port exposed",
},
},
previewUrl: exposed.url,
});
} Test it:
Test it:
Test it:
curl -X POST http://localhost:8787/preview {
"steps": {
"clone": { "status": "success", "message": "Already cloned" },
"install": { "status": "success", "message": "Dependencies installed" },
"server": {
"status": "success",
"message": "Dev server running",
"processId": "proc_abc123"
},
"expose": { "status": "success", "message": "Port exposed" }
},
"previewUrl": "http://localhost:8787/proxy/preview-env/http/8080/"
} Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.
Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.
Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.
Bước 5: Stream log process Step 5: Stream Process Logs ជំហាន 5: Stream log process
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- Real-time log output from your dev server, streamed to the client.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- Logs help you debug startup failures and monitor running processes — this is a genuinely separate endpoint since it returns a stream, not JSON.
Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:
Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:
Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:
import { parseSSEStream, type LogEvent } from "@cloudflare/sandbox";
if (url.pathname === "/preview/logs") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
const processId = url.searchParams.get("pid");
if (!processId) {
return Response.json(
{ error: "pid query param required" },
{ status: 400 },
);
}
const logStream = await previewSandbox.streamProcessLogs(processId);
return new Response(logStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
} Test it using the processId from your /preview response:
Test it using the processId from your /preview response:
Test it using the processId from your /preview response:
curl "http://localhost:8787/preview/logs?pid=proc_abc123" You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:
You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:
You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:
// In your fetch handler signature: fetch(request, env, ctx)
ctx.waitUntil(
(async () => {
const logStream = await previewSandbox.streamProcessLogs(server.id);
for await (const log of parseSSEStream<LogEvent>(logStream)) {
console.log("[preview-server]", log.data);
}
})(),
); Bước 6: Sẵn sàng production Step 6: Make It Production-Ready ជំហាន 6: រៀបចំឲ្យសម្រាប់ production
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- A robust preview endpoint with error handling and process cleanup.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- Repeated calls shouldn't create duplicate processes, and failures should return clear error messages — not crash your Worker.
The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:
The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:
The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:
if (url.pathname === "/preview" && request.method === "POST") {
const previewSandbox = getSandbox(env.Sandbox, "preview-env");
const hostname = new URL(request.url).hostname;
try {
// 1. Clone if needed
const { exists: alreadyCloned } = await previewSandbox.exists(
"/workspace/vite-react",
);
if (!alreadyCloned) {
await previewSandbox.gitCheckout(
"https://github.com/harshil1712/vite-react.git",
);
}
// 2. Install dependencies
const install = await previewSandbox.exec("npm install", {
cwd: "/workspace/vite-react",
timeout: 120000,
});
// 3. Kill any existing dev server to avoid duplicates
const processes = await previewSandbox.listProcesses();
for (const proc of processes) {
if (proc.command.includes("npm run dev")) {
await previewSandbox.killProcess(proc.id);
}
}
// 4. Start the dev server
const server = await previewSandbox.startProcess(
"npm run dev -- --port 8080",
{ cwd: "/workspace/vite-react" },
);
await server.waitForPort(8080, { timeout: 30000 });
// 5. Expose the port
const exposed = await previewSandbox.exposePort(8080, { hostname });
return Response.json({
steps: {
clone: {
status: "success",
message: alreadyCloned ? "Already cloned" : "Repository cloned",
},
install: {
status: install.exitCode === 0 ? "success" : "failed",
message:
install.exitCode === 0 ? "Dependencies installed" : install.stderr,
},
server: {
status: "success",
message: "Dev server running",
processId: server.id,
},
expose: {
status: "success",
message: "Port exposed",
},
},
previewUrl: exposed.url,
});
} catch (err) {
return Response.json(
{ error: err instanceof Error ? err.message : "Preview setup failed" },
{ status: 500 },
);
}
} This is the final version. Compared to Step 4’s code:
This is the final version. Compared to Step 4’s code:
This is the final version. Compared to Step 4’s code:
- Process cleanup — listProcesses() and killProcess() stop any existing dev server before starting a new one, so repeated calls are safe
- Error handling — try/catch wraps the entire flow, returning a clear 500 error instead of an unhandled exception
- Same response shape — the steps object still reports per-step status, so debugging is straightforward
Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.
Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.
Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.