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