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>