1---
2title: Nuxt
3description: Deploy a Nuxt app to Cloudflare Workers with Cloudflare.Website.Nuxt — nitro's cloudflare_module preset, native nuxt.config.ts loading, and wrangler-free local dev.
4sidebar:
5 order: 10
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Cloudflare.Website.Nuxt` deploys a [Nuxt](https://nuxt.com) app as
13a Cloudflare Worker. It builds the app through your project's own
14`@nuxt/kit` with nitro's `cloudflare_module` preset: the nitro server
15bundle deploys as the Worker script, and client assets plus
16prerendered pages deploy as Worker static assets. There is no
17`nitro.preset` to edit, no Wrangler file, and no build command to
18run.
19
20## Install
21
22The build integration is not bundled with alchemy. Install
23`@alchemy.run/frontend-frameworks`; the resource loads its `/nuxt`
24export from your project at deploy time. It is only used at build time,
25so 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## Configure Nuxt
43
44Your `nuxt.config.ts` loads natively — modules, layers, and all —
45so configure Nuxt exactly as you would outside Alchemy:
46
47```typescript
48// nuxt.config.ts
49export default defineNuxtConfig({
50 modules: ["@nuxtjs/tailwindcss"],
51 routeRules: {
52 "/about": { prerender: true },
53 },
54});
55```
56
57Don't set `nitro.preset` — the Cloudflare deploy target owns the
58preset, and a foreign preset is a hard error.
59
60## Deploy-time overrides
61
62Config that only exists at deploy time — a stage-dependent base URL, a
63per-environment `runtimeConfig` — goes in the `nuxt` prop. It merges
64over `nuxt.config.ts` as the highest-priority layer, so a value here
65wins:
66
67```typescript
68export const Website = Cloudflare.Website.Nuxt("Website", {
69 nuxt: {
70 app: { baseURL: "/docs/" },
71 runtimeConfig: {
72 public: { apiBase: "https://api.example.com" },
73 },
74 },
75});
76```
77
78The bag must be JSON-serializable — no functions, plugins, or modules;
79those belong in `nuxt.config.ts`. `nitro.preset` stays owned by the
80deploy target.
81
82## Declare the Website
83
84Declare the site as a module-level const (rather than inline in the
85Stack) and derive the typed shape of its bindings from it:
86
87```typescript
88// alchemy.run.ts
89import * as Cloudflare from "alchemy/Cloudflare";
90
91export const Website = Cloudflare.Website.Nuxt("Website");
92
93export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
94```
95
96`WebsiteEnv` is the typed shape of the Worker's bindings, derived
97from the class — you'll import it into your Nuxt code in
98[Read bindings in server code](#read-bindings-in-server-code).
99
100## Add it to the Stack
101
102Yield the class from your Stack and return its URL:
103
104```typescript
105// alchemy.run.ts
106import * as Alchemy from "alchemy";
107import * as Effect from "effect/Effect";
108
109export default Alchemy.Stack(
110 "MyNuxtSite",
111 {
112 providers: Cloudflare.providers(),
113 state: Cloudflare.state(),
114 },
115 Effect.gen(function* () {
116 const site = yield* Website;
117 return { url: site.url };
118 }),
119);
120```
121
122The server build uses nitro's hybrid workerd Node compatibility, so
123the `nodejs_compat` compatibility flag is added automatically — you
124don't need to pass it.
125
126See
127[examples/cloudflare-website-nuxt](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-nuxt)
128for the checked-in example.
129
130## Add bindings
131
132The resource returns a plain `Worker`, so `env` accepts the full
133binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
134secrets:
135
136```diff lang="typescript"
137// alchemy.run.ts
138+import * as Config from "effect/Config";
139
140+export const Uploads = Cloudflare.R2.Bucket("Uploads");
141
142export const Website = Cloudflare.Website.Nuxt("Website", {
143+ env: {
144+ UPLOADS: Uploads,
145+ API_KEY: Config.Redacted("API_KEY"),
146+ },
147});
148```
149
150`Uploads` is a description, not a deploy — Alchemy provisions the
151real bucket because the Website binds it. `Config.Redacted` reads
152`API_KEY` from your environment at deploy time and binds it as a
153Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
154
155## Read bindings in server code
156
157Server routes and SSR read bindings through nitro's Cloudflare
158runtime contract, `event.context.cloudflare.env`. Type it once by
159augmenting h3's event context with the inferred env type:
160
161```typescript
162// server/types.d.ts
163import type { WebsiteEnv } from "../alchemy.run.ts";
164
165declare module "h3" {
166 interface H3EventContext {
167 cloudflare: {
168 env: WebsiteEnv;
169 };
170 }
171}
172```
173
174Every server route now gets fully typed bindings:
175
176```typescript
177// server/api/upload.ts
178export default defineEventHandler(async (event) => {
179 const uploads = event.context.cloudflare.env.UPLOADS;
180 await uploads.put("hello.txt", "Hello!");
181 return { ok: true };
182});
183```
184
185`event.context.cloudflare.env.UPLOADS` is typed as an R2 bucket and
186`.API_KEY` as a string — renaming a binding in `alchemy.run.ts` is a
187type error in your routes. `event.context.cf` and
188`event.context.cloudflare.context.waitUntil` are available the same
189way.
190
191## Prerendering
192
193Routes marked for prerendering in `routeRules` (or via
194`nitro.prerender`) render at build time into `.output/public` and are
195served as static assets, with no Worker invocation. Configure them in
196your `nuxt.config.ts`, which loads natively:
197
198```typescript
199// nuxt.config.ts
200export default defineNuxtConfig({
201 routeRules: {
202 "/about": { prerender: true },
203 },
204});
205```
206
207Nitro's `isr` route rule is implemented only by the Vercel and
208Netlify presets. On Cloudflare it is silently ignored at build time,
209and the route renders on demand in the Worker like any other SSR
210route. Use `prerender` for build-time static routes, or `cache` route
211rules for runtime caching.
212
213## Custom Worker exports (Durable Objects)
214
215Nitro's entry module is the Worker's exports seam. Point `main` at
216your own module that re-exports nitro's runtime handler and adds
217extra exports — Durable Object classes must live on the deployed
218Worker for their namespace bindings to resolve:
219
220```typescript
221// worker-entry.ts
222import nitroHandler from "nitropack/presets/cloudflare/runtime/cloudflare-module";
223export class Counter extends DurableObject {}
224export default nitroHandler;
225```
226
227```typescript
228export const Website = Cloudflare.Website.Nuxt("Website", {
229 main: "worker-entry.ts",
230 env: {
231 COUNTER: Cloudflare.DurableObject("Counter", {
232 className: "Counter",
233 }),
234 },
235});
236```
237
238Every framework route keeps working through the re-exported handler.
239
240## Local dev
241
242```sh
243bun alchemy dev
244```
245
246`alchemy dev` runs Nuxt's own dev server (nitro dev, full HMR) with
247the Worker's real bindings served on `event.context.cloudflare` —
248wrangler-free, through the cloudflare-runtime platform proxy.
249Resource bindings (KV, R2, D1) round-trip to a local workerd
250instance, so dev state is live and shared; literal `env` values
251overlay the proxied bindings. Your server code is identical in dev
252and production.
253
254One limitation: Durable Object classes declared via a custom `main`
255entry only exist in the production build, so they are not servable in
256dev yet.