1---
2title: Vue
3description: Deploy a Vue single-page app to AWS with AWS.Website.Vite — S3 + CloudFront, deep links that work by default, and Vue's own Vite dev server under alchemy dev.
4sidebar:
5 order: 6
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11export const routerDep = "vue-router";
12
13[Vue](https://vuejs.org) is pure Vite — the whole app builds from
14`@vitejs/plugin-vue` in your `vite.config.ts`, so
15[`AWS.Website.Vite`](/aws/frontend/vite) deploys it with a single
16declaration. The build output goes to a private S3 bucket behind a
17CloudFront distribution; there is no server function, no build command
18to wire up, and no CloudFormation to write.
19
20## Install
21
22The build integration is not bundled with alchemy. Install
23`@alchemy.run/frontend-frameworks`; the resource loads its `/vite` and
24`/vite/aws` exports from your project at deploy time. It is only used at
25build time, so a dev dependency is enough:
26
27<Tabs syncKey="pkgManager">
28 <TabItem label="bun" icon="bun">
29 <Code code={`bun add -d ${devDeps}`} lang="sh" />
30 </TabItem>
31 <TabItem label="npm" icon="npm">
32 <Code code={`npm install -D ${devDeps}`} lang="sh" />
33 </TabItem>
34 <TabItem label="pnpm" icon="pnpm">
35 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
36 </TabItem>
37 <TabItem label="yarn" icon="seti:yarn">
38 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
39 </TabItem>
40</Tabs>
41
42## Scaffold the app
43
44Start from Vite's official Vue + TypeScript template:
45
46<Tabs syncKey="pkgManager">
47 <TabItem label="bun" icon="bun">
48 <Code code={`bun create vite@latest my-vue-app -- --template vue-ts\ncd my-vue-app && bun install`} lang="sh" />
49 </TabItem>
50 <TabItem label="npm" icon="npm">
51 <Code code={`npm create vite@latest my-vue-app -- --template vue-ts\ncd my-vue-app && npm install`} lang="sh" />
52 </TabItem>
53 <TabItem label="pnpm" icon="pnpm">
54 <Code code={`pnpm create vite my-vue-app --template vue-ts\ncd my-vue-app && pnpm install`} lang="sh" />
55 </TabItem>
56 <TabItem label="yarn" icon="seti:yarn">
57 <Code code={`yarn create vite my-vue-app --template vue-ts\ncd my-vue-app && yarn install`} lang="sh" />
58 </TabItem>
59</Tabs>
60
61Write `alchemy.run.ts` in that project root, next to `index.html` and
62`vite.config.ts`. `rootDir` defaults to `"."`, so you only set it when the
63app lives somewhere else.
64
65## Configure Vite
66
67Your Vite config stays exactly what the template gives you:
68
69```typescript
70// vite.config.ts
71import { defineConfig } from "vite";
72import vue from "@vitejs/plugin-vue";
73
74export default defineConfig({
75 plugins: [vue()],
76});
77```
78
79Alchemy runs your project's own `vite build` programmatically against this
80config — plugins, aliases, and the rest of your setup are preserved as-is.
81
82## Declare the Website
83
84Declare the site as a module-level const (rather than inline in the
85Stack) — every prop is optional, so the minimal declaration is just a
86name:
87
88```typescript
89// alchemy.run.ts
90import * as AWS from "alchemy/AWS";
91
92export const Website = AWS.Website.Vite("Vue");
93```
94
95## Add it to the Stack
96
97Yield the site from your Stack and return its URL:
98
99```typescript
100// alchemy.run.ts
101import * as Alchemy from "alchemy";
102import * as Effect from "effect/Effect";
103
104export default Alchemy.Stack(
105 "MyVueSite",
106 {
107 providers: AWS.providers(),
108 state: AWS.state(),
109 },
110 Effect.gen(function* () {
111 const site = yield* Website;
112 return { url: site.url };
113 }),
114);
115```
116
117```sh
118bun alchemy deploy
119```
120
121Alchemy runs `vite build`, uploads `dist` to S3, and serves it from
122CloudFront at the returned `url`. Input files are content-hashed, so a
123deploy that changes nothing skips the build and the upload.
124
125## Deep links with vue-router
126
127`vue-router` in history mode (`createWebHistory`) sends a deep link like
128`/about` to the server as a request for a file that doesn't exist.
129`AWS.Website.Vite` answers those misses with the index page and a `200`,
130so the router resolves the route on the client — that is the `spa`
131default and there is nothing to configure.
132
133<Tabs syncKey="pkgManager">
134 <TabItem label="bun" icon="bun">
135 <Code code={`bun add ${routerDep}`} lang="sh" />
136 </TabItem>
137 <TabItem label="npm" icon="npm">
138 <Code code={`npm install ${routerDep}`} lang="sh" />
139 </TabItem>
140 <TabItem label="pnpm" icon="pnpm">
141 <Code code={`pnpm add ${routerDep}`} lang="sh" />
142 </TabItem>
143 <TabItem label="yarn" icon="seti:yarn">
144 <Code code={`yarn add ${routerDep}`} lang="sh" />
145 </TabItem>
146</Tabs>
147
148```typescript
149// src/main.ts
150import { createApp } from "vue";
151import { createRouter, createWebHistory } from "vue-router";
152import App from "./App.vue";
153import About from "./About.vue";
154import Home from "./Home.vue";
155
156const router = createRouter({
157 history: createWebHistory(),
158 routes: [
159 { path: "/", component: Home },
160 { path: "/about", component: About },
161 ],
162});
163
164createApp(App).use(router).mount("#app");
165```
166
167Loading `https://<your-site>/about` directly serves the index page and
168vue-router renders the About view. Pretty URLs still win over the
169fallback: `/about` serves `about.html` or `about/index.html` when the
170build produced one.
171
172For a multi-page project (one `index.html` per route), turn the fallback
173off and serve a real 404 instead:
174
175```diff lang="typescript"
176// alchemy.run.ts
177export const Website = AWS.Website.Vite("Vue", {
178+ spa: false,
179+ errorPage: "404.html",
180});
181```
182
183`spa` and `errorPage` are mutually exclusive: one answers misses with the
184index page at `200`, the other with your built error page at `404`.
185
186## Add environment variables
187
188A pure SPA has no server, so anything the app needs is baked into the
189bundle at build time. Vite inlines `VITE_`-prefixed variables from your
190project's `.env` files and from the environment the deploy runs in:
191
192```sh
193# .env.production
194VITE_API_URL=https://api.example.com
195```
196
197Type it for your Vue code with Vite's standard `ImportMetaEnv`
198augmentation:
199
200```typescript
201// src/vite-env.d.ts
202/// <reference types="vite/client" />
203
204interface ImportMetaEnv {
205 readonly VITE_API_URL: string;
206}
207```
208
209Client code reads it as `import.meta.env.VITE_API_URL`.
210
211## Bake a Stack output into the bundle
212
213To inline a value that only exists after another resource deploys — an
214API's URL, a bucket name — build the site with
215[`AWS.Website.StaticSite`](/aws/frontend/static-site), whose
216`build.env` is passed to the build command:
217
218```typescript
219// alchemy.run.ts
220export default Alchemy.Stack(
221 "MyVueSite",
222 {
223 providers: AWS.providers(),
224 state: AWS.state(),
225 },
226 Effect.gen(function* () {
227 const api = yield* Api;
228
229 const site = yield* AWS.Website.StaticSite("Vue", {
230 build: {
231 command: "vite build",
232 output: "dist",
233 env: {
234 VITE_API_URL: api.url,
235 },
236 },
237 spa: true,
238 });
239
240 return { url: site.url };
241 }),
242);
243```
244
245The build re-runs when the inputs change and the output deploys to the
246same S3 + CloudFront stack.
247
248## Local dev
249
250```sh
251bun alchemy dev
252```
253
254`alchemy dev` runs Vue's own Vite dev server with native HMR instead of
255deploying; `site.url` is the local address and no AWS resources are
256created. Wrap the site in `Alchemy.remote()` to deploy the real
257infrastructure even during dev.
258
259## Custom domain
260
261```typescript
262// alchemy.run.ts
263export const Website = AWS.Website.Vite("Vue", {
264 domain: { name: "app.example.com" },
265});
266```
267
268The ACM certificate and the Route 53 records are created for you, and
269`site.url` becomes `https://app.example.com`. The hosted zone is
270inferred from the hostname — pass `hostedZoneId` to pin it when
271several zones could match.
272
273## Where next
274
275- [Vite on AWS](/aws/frontend/vite) — every prop the resource takes,
276 including `vite.outDir`, `vite.base`, and Router composition
277- [Websites on AWS](/aws/frontend/websites) — the full websites surface