Skip to content

Nuxt

Integrate PutPut with Nuxt 3 using a server route for presigning and a composable for the upload flow.

1. Environment variable

// .env
PUTPUT_TOKEN=pp_guest_...

2. Server route for presign + confirm

// server/api/upload/presign.post.ts
const API = "https://putput.io/api/v1";

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const config = useRuntimeConfig();

  const res = await fetch(`${API}/upload/presign`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${config.putputToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return res.json();
});

// server/api/upload/confirm.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const config = useRuntimeConfig();

  const res = await fetch(`${API}/upload/confirm`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${config.putputToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return res.json();
});

3. Composable

// composables/usePutPut.ts
export function usePutPut() {
  const uploading = ref(false);
  const publicUrl = ref<string | null>(null);

  async function upload(file: File) {
    uploading.value = true;

    // 1. Presign via our server route
    const presign = await $fetch("/api/upload/presign", {
      method: "POST",
      body: { filename: file.name, content_type: file.type, size_bytes: file.size },
    });

    // 2. Upload directly to R2
    await fetch(presign.presigned_url, {
      method: "PUT",
      headers: { "Content-Type": file.type },
      body: file,
    });

    // 3. Confirm
    const result = await $fetch("/api/upload/confirm", {
      method: "POST",
      body: { upload_id: presign.upload_id },
    });

    publicUrl.value = result.file.public_url;
    uploading.value = false;
  }

  return { upload, uploading, publicUrl };
}

4. Component

<!-- components/FileUpload.vue -->
<script setup lang="ts">
const { upload, uploading, publicUrl } = usePutPut();

function handleFile(e: Event) {
  const input = e.target as HTMLInputElement;
  if (input.files?.[0]) upload(input.files[0]);
}
</script>

<template>
  <div>
    <input type="file" @change="handleFile" :disabled="uploading" />
    <p v-if="uploading">Uploading...</p>
    <a v-if="publicUrl" :href="publicUrl">{{ publicUrl }}</a>
  </div>
</template>
v0.4.77