Skip to content

Cloudflare Nuxt 3 with Data Pipeline

This guide walks through deploying a full-stack Nuxt 3 application with a backend Pipeline to Cloudflare using Alchemy.

Create a new Nuxt 3 Project

Start by creating a new Nuxt 3 project:

sh
bun create nuxt-app 
cd my-nuxt-app
bun install

Install alchemy and Cloudflare:

sh
bun add alchemy cloudflare

Configure Nuxt for Cloudflare

Update nuxt.config.ts to work with Cloudflare Workers:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: "2025-04-21",
  devtools: { enabled: true },
  nitro: {
    preset: "cloudflare-module",
    prerender: {
      routes: ["/"],
      autoSubfolderIndex: false,
    },
  },
});

Create alchemy.run.ts

Create an alchemy.run.ts file in the root of your project. We'll build this file step by step:

1. Set up imports and initialize app

typescript
// ./alchemy.run.ts
import alchemy from "alchemy";
import { Pipeline, R2Bucket, Nuxt } from "alchemy/cloudflare";

const R2_BUCKET_NAME = "example-bucket";
const PIPELINE_NAME = "example-pipeline";

const app = await alchemy("nuxt-pipeline-app", {
  stage: process.env.USER ?? "dev",
  phase: process.argv.includes("--destroy") ? "destroy" : "up",
  quiet: !process.argv.includes("--verbose"),
  password: process.env.ALCHEMY_PASS,
});

2. Create R2 bucket for data storage

typescript
const bucket = await R2Bucket("bucket", {
  name: R2_BUCKET_NAME,
});

3. Configure data pipeline

typescript
const pipeline = await Pipeline("pipeline", {
  name: PIPELINE_NAME,
  source: [{ type: "binding", format: "json" }],
  destination: {
    type: "r2",
    format: "json",
    path: {
      bucket: bucket.name,
    },
    credentials: {
      accessKeyId: alchemy.secret(process.env.R2_ACCESS_KEY_ID),
      secretAccessKey: alchemy.secret(process.env.R2_SECRET_ACCESS_KEY),
    },
    batch: {
      maxMb: 10,
      maxSeconds: 5,
      maxRows: 100,
    },
  },
});

CAUTION

Set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and ALCHEMY_PASS environment variables before deployment.

4. Configure Nuxt website with bindings

typescript
export const website = await Nuxt("website", {
  bindings: {
    R2_BUCKET: bucket,
    PIPELINE: pipeline,
  },
});

console.log({
  url: website.url,
});

await app.finalize();

Infer Binding Types

Create an src/env.d.ts file to support type hints for Cloudflare bindings:

typescript
// src/env.d.ts
/// <reference types="@cloudflare/workers-types" />

import type { website } from './alchemy.run';

export type WorkerEnv = typeof website.Env;

declare module 'cloudflare:workers' {
  namespace Cloudflare {
    export interface Env extends WorkerEnv {}
  }
}

Add API Route for Pipeline

Create a Nuxt server API route to send data to the pipeline:

typescript
// server/api/pipeline.post.ts
import { env } from "cloudflare:workers";

export default defineEventHandler(async (event) => {
  try {
    const body = await readBody(event);
    const pipeline = env.PIPELINE;
    const data = body.data;

    if (!data) {
      throw new Error("Missing 'data' property in request body");
    }

    await pipeline.send([{ value: data }]);

    return { success: true, message: "Data sent to pipeline." };
  } catch (error) {
    console.error("Error sending data to pipeline:", error);
    throw createError({
      statusCode: 500,
      statusMessage: error instanceof Error ? error.message : "Pipeline error",
    });
  }
});

Create Frontend Interface

Create a simple form to interact with the pipeline:

vue
<!-- pages/index.vue -->
<template>
  <div>
    <h1>Nuxt 3 + Alchemy + Cloudflare Pipeline Demo</h1>
    <form @submit.prevent="sendToPipeline">
      <label for="dataInput">Data to send:</label>
      <input id="dataInput" v-model="dataToSend" type="text" required />
      <button type="submit" :disabled="loading">Send to Pipeline</button>
    </form>
    <p v-if="message">{{ message }}</p>
    <p v-if="error" style="color: red">{{ error }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const dataToSend = ref("");
const loading = ref(false);
const message = ref("");
const error = ref("");

async function sendToPipeline() {
  loading.value = true;
  message.value = "";
  error.value = "";

  try {
    const response = await fetch("/api/pipeline", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ data: dataToSend.value }),
    });

    const result = await response.json();

    if (!response.ok) {
      throw new Error(result.statusMessage || "Failed to send data");
    }

    message.value = result.message || "Data sent successfully!";
    dataToSend.value = "";
  } catch (err) {
    error.value =
      err instanceof Error ? err.message : "An unknown error occurred.";
    console.error("Error sending to pipeline:", err);
  } finally {
    loading.value = false;
  }
}
</script>

<style scoped>
form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 300px;
  margin-top: 20px;
}
button {
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Deploy Your Application

Login to Cloudflare:

sh
wrangler login

Run your Alchemy script to deploy the application:

sh
bun ./alchemy.run

It should output the URL of your deployed site:

sh
{
  url: "https://your-site.your-account.workers.dev"
}

Click the URL to see your site. Test sending data via the form; it should appear in your R2 bucket shortly after.

Local Development

To run your application locally, use the Nuxt development server:

sh
bun run dev

This will start a local development server:

sh
Nuxt 3.9.0 with Nitro 2.8.1
 
 Local:    http://localhost:3000/
 Network:  use --host to expose this

Tear Down

When you're finished experimenting, you can tear down the application:

sh
bun ./alchemy.run --destroy