Skip to content

Part 4: A Vite Frontend

Continue from Part 3. Add a browser button that queries the API, then run the frontend locally against the deployed backend.

Terminal window
bun add -d vite @vercel/nft

Vite builds the frontend. @vercel/nft traces the server dependencies that Alchemy includes in the deployment archive.

The frontend and API have separate origins. Add a response-header variable inside the API’s fetch Effect:

src/Api.ts
const request = yield* HttpServerRequest;
const headers = { "access-control-allow-origin": "*" };

This tutorial exposes only a public, read-only timestamp query. For a real application, restrict allowed origins and add authentication before exposing private data. CORS is a browser policy, not an authorization mechanism.

src/Api.ts
status: 405,
headers: { allow: "GET" },
headers: { ...headers, allow: "GET" },
return yield* HttpServerResponse.json({ ok: true });
return yield* HttpServerResponse.json({ ok: true }, { headers });
return yield* HttpServerResponse.json(rows[0]);
return yield* HttpServerResponse.json(rows[0], { headers });
return HttpServerResponse.text("Not found", { status: 404 });
return HttpServerResponse.text("Not found", { status: 404, headers });
status: 503,
headers: { "access-control-allow-origin": "*" },

All explicitly constructed API responses now use the same CORS policy. The frontend sends a simple GET without credentials or custom headers.

Create index.html in the project root:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prisma tutorial</title>
</head>
<body>
<main>
<h1>Prisma database clock</h1>
<p>Read the current time from Prisma Postgres through an Effect API.</p>
<button id="refresh" type="button">Read database time</button>
<p id="result" role="status">No query yet.</p>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

The status element announces query results, while the button gives the user control over when to send a request.

Create src/main.ts:

const button = document.querySelector<HTMLButtonElement>("#refresh")!;
const result = document.querySelector<HTMLParagraphElement>("#result")!;
button.addEventListener("click", async () => {
button.disabled = true;
result.textContent = "Querying Postgres…";
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/time`);
if (!response.ok) throw new Error(`Request failed (${response.status})`);
const data: { time: string } = await response.json();
result.textContent = `Database time: ${data.time}`;
} catch {
result.textContent = "Could not read the database. Please try again.";
} finally {
button.disabled = false;
}
});

The button is disabled during the request and re-enabled after either success or failure. VITE_API_URL will contain a public API URL, never a database credential. If your editor needs Vite’s import.meta.env types, add /// <reference types="vite/client" /> to src/vite-env.d.ts.

Keep the backend remote during development

Section titled “Keep the backend remote during development”
alchemy.run.ts
const project = yield* Project;
const api = yield* Api;
const project = yield* Project.pipe(Alchemy.remote());
const api = yield* Api.pipe(Alchemy.remote());

Alchemy.remote() keeps this backend on Prisma when running alchemy dev. It is a no-op during deployment. This tutorial deliberately uses real Postgres during frontend development, so credentials and cloud charges still apply. The Project is also remote because it is shared with the website.

alchemy.run.ts
const api = yield* Api.pipe(Alchemy.remote());
const site = yield* Prisma.Website.Vite("Website", {
project,
regionId: "eu-west-3",
env: { VITE_API_URL: api.url },
});
return { projectId: project.projectId, apiUrl: api.url };
return { url: site.url, apiUrl: api.url };

The website uses the same Project. Its environment references the API output, so Alchemy resolves the API URL before building the frontend. VITE_ values are embedded in the public browser bundle; only put public configuration there.

Terminal window
bun alchemy deploy

Open url and click Read database time. The response should display a Postgres timestamp. Click again to execute a new query.

Terminal window
bun alchemy dev

Open the local website URL that Alchemy prints. Edit the heading and confirm Vite updates the page. Click the button to query the remote API. Provider-mode changes can replace the deployed website with its local counterpart in this stage; use a separate --stage dev when you want to keep a production site. A separate stage creates its own remote backend resources too.

Stop the dev command, then destroy the Stack:

Terminal window
bun alchemy destroy

Confirm the deletion. This removes the website, API, connection, database, and Project. Use the same profile and stage used to deploy; destroy every extra stage you created. Keep .alchemy/ until cleanup completes so Alchemy can find the resources it owns.