1---
2title: "Part 4: A Vite Frontend"
3description: Connect a Vite frontend to your Prisma API, develop locally, and destroy the tutorial resources.
4sidebar:
5 order: 4
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10Continue from [Part 3](/prisma/tutorial/part-3). Add a browser button that
11queries the API, then run the frontend locally against the deployed backend.
12
13## Install the frontend tooling
14
15<Tabs syncKey="pkgManager">
16 <TabItem label="bun" icon="bun"><Code code="bun add -d vite @vercel/nft" lang="sh" /></TabItem>
17 <TabItem label="npm" icon="npm"><Code code="npm install -D vite @vercel/nft" lang="sh" /></TabItem>
18 <TabItem label="pnpm" icon="pnpm"><Code code="pnpm add -D vite @vercel/nft" lang="sh" /></TabItem>
19 <TabItem label="yarn" icon="seti:yarn"><Code code="yarn add -D vite @vercel/nft" lang="sh" /></TabItem>
20</Tabs>
21
22Vite builds the frontend. `@vercel/nft` traces the server dependencies that
23Alchemy includes in the deployment archive.
24
25## Allow the frontend origin
26
27The frontend and API have separate origins. Add a response-header variable
28inside the API's `fetch` Effect:
29
30```diff lang="typescript"
31// src/Api.ts
32 const request = yield* HttpServerRequest;
33+ const headers = { "access-control-allow-origin": "*" };
34```
35
36This tutorial exposes only a public, read-only timestamp query. For a real
37application, restrict allowed origins and add authentication before exposing
38private data. CORS is a browser policy, not an authorization mechanism.
39
40## Attach the response headers
41
42```diff lang="typescript"
43// src/Api.ts
44 status: 405,
45- headers: { allow: "GET" },
46+ headers: { ...headers, allow: "GET" },
47
48- return yield* HttpServerResponse.json({ ok: true });
49+ return yield* HttpServerResponse.json({ ok: true }, { headers });
50
51- return yield* HttpServerResponse.json(rows[0]);
52+ return yield* HttpServerResponse.json(rows[0], { headers });
53
54- return HttpServerResponse.text("Not found", { status: 404 });
55+ return HttpServerResponse.text("Not found", { status: 404, headers });
56
57 status: 503,
58+ headers: { "access-control-allow-origin": "*" },
59```
60
61All explicitly constructed API responses now use the same CORS policy.
62The frontend sends a simple GET without credentials or custom headers.
63
64## Create the page
65
66Create `index.html` in the project root:
67
68```html
69<!doctype html>
70<html lang="en">
71 <head>
72 <meta charset="UTF-8" />
73 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
74 <title>Prisma tutorial</title>
75 </head>
76 <body>
77 <main>
78 <h1>Prisma database clock</h1>
79 <p>Read the current time from Prisma Postgres through an Effect API.</p>
80 <button id="refresh" type="button">Read database time</button>
81 <p id="result" role="status">No query yet.</p>
82 </main>
83 <script type="module" src="/src/main.ts"></script>
84 </body>
85</html>
86```
87
88The status element announces query results, while the button gives the user
89control over when to send a request.
90
91## Query from the browser
92
93Create `src/main.ts`:
94
95```typescript
96const button = document.querySelector<HTMLButtonElement>("#refresh")!;
97const result = document.querySelector<HTMLParagraphElement>("#result")!;
98
99button.addEventListener("click", async () => {
100 button.disabled = true;
101 result.textContent = "Querying Postgres…";
102 try {
103 const response = await fetch(`${import.meta.env.VITE_API_URL}/api/time`);
104 if (!response.ok) throw new Error(`Request failed (${response.status})`);
105 const data: { time: string } = await response.json();
106 result.textContent = `Database time: ${data.time}`;
107 } catch {
108 result.textContent = "Could not read the database. Please try again.";
109 } finally {
110 button.disabled = false;
111 }
112});
113```
114
115The button is disabled during the request and re-enabled after either success
116or failure. `VITE_API_URL` will contain a public API URL, never a database
117credential. If your editor needs Vite's `import.meta.env` types, add
118`/// <reference types="vite/client" />` to `src/vite-env.d.ts`.
119
120## Keep the backend remote during development
121
122```diff lang="typescript"
123// alchemy.run.ts
124- const project = yield* Project;
125- const api = yield* Api;
126+ const project = yield* Project.pipe(Alchemy.remote());
127+ const api = yield* Api.pipe(Alchemy.remote());
128```
129
130`Alchemy.remote()` keeps this backend on Prisma when running `alchemy dev`.
131It is a no-op during deployment. This tutorial deliberately uses real
132Postgres during frontend development, so credentials and cloud charges still
133apply. The Project is also remote because it is shared with the website.
134
135## Add the website
136
137```diff lang="typescript"
138// alchemy.run.ts
139 const api = yield* Api.pipe(Alchemy.remote());
140+ const site = yield* Prisma.Website.Vite("Website", {
141+ project,
142+ regionId: "eu-west-3",
143+ env: { VITE_API_URL: api.url },
144+ });
145- return { projectId: project.projectId, apiUrl: api.url };
146+ return { url: site.url, apiUrl: api.url };
147```
148
149The website uses the same Project. Its environment references the API output,
150so Alchemy resolves the API URL before building the frontend. `VITE_` values
151are embedded in the public browser bundle; only put public configuration there.
152
153## Deploy the frontend
154
155<Tabs syncKey="pkgManager">
156 <TabItem label="bun" icon="bun"><Code code="bun alchemy deploy" lang="sh" /></TabItem>
157 <TabItem label="npm" icon="npm"><Code code="npx alchemy deploy" lang="sh" /></TabItem>
158 <TabItem label="pnpm" icon="pnpm"><Code code="pnpm exec alchemy deploy" lang="sh" /></TabItem>
159 <TabItem label="yarn" icon="seti:yarn"><Code code="yarn alchemy deploy" lang="sh" /></TabItem>
160</Tabs>
161
162Open `url` and click **Read database time**. The response should display a
163Postgres timestamp. Click again to execute a new query.
164
165## Develop locally
166
167<Tabs syncKey="pkgManager">
168 <TabItem label="bun" icon="bun"><Code code="bun alchemy dev" lang="sh" /></TabItem>
169 <TabItem label="npm" icon="npm"><Code code="npx alchemy dev" lang="sh" /></TabItem>
170 <TabItem label="pnpm" icon="pnpm"><Code code="pnpm exec alchemy dev" lang="sh" /></TabItem>
171 <TabItem label="yarn" icon="seti:yarn"><Code code="yarn alchemy dev" lang="sh" /></TabItem>
172</Tabs>
173
174Open the local website URL that Alchemy prints. Edit the heading and confirm
175Vite updates the page. Click the button to query the remote API. Provider-mode
176changes can replace the deployed website with its local counterpart in this
177stage; use a separate `--stage dev` when you want to keep a production site.
178A separate stage creates its own remote backend resources too.
179
180## Remove the resources
181
182Stop the dev command, then destroy the Stack:
183
184<Tabs syncKey="pkgManager">
185 <TabItem label="bun" icon="bun"><Code code="bun alchemy destroy" lang="sh" /></TabItem>
186 <TabItem label="npm" icon="npm"><Code code="npx alchemy destroy" lang="sh" /></TabItem>
187 <TabItem label="pnpm" icon="pnpm"><Code code="pnpm exec alchemy destroy" lang="sh" /></TabItem>
188 <TabItem label="yarn" icon="seti:yarn"><Code code="yarn alchemy destroy" lang="sh" /></TabItem>
189</Tabs>
190
191Confirm the deletion. This removes the website, API, connection, database, and
192Project. Use the same profile and stage used to deploy; destroy every extra
193stage you created. Keep `.alchemy/` until cleanup completes so Alchemy can
194find the resources it owns.
195
196## Where to go next
197
198- [Complete tutorial example](https://github.com/alchemy-run/alchemy/tree/main/examples/prisma-tutorial)
199- [Frontend frameworks](/prisma/frontend/websites) — Astro, Next.js, and the other Website helpers.
200- [Postgres](/prisma/data/postgres) — database configuration and local development.
201- [Connections](/prisma/data/connections) — runtime connection bindings.
202- [Compute apps](/prisma/compute/apps) — deployment options and custom domains.