Skip to content
PutPut is shutting down on October 21, 2026. New signups, uploads, and paid plans are closed. Please download your files before then.

Next.js

Integrate PutPut with Next.js App Router using Server Actions. No SDK required — plain fetch calls.

1. Get a token

Store the token server-side or pass it to the client via an environment variable.

// .env.local
PUTPUT_TOKEN=pp_guest_...

2. Server Action for presign + confirm

// app/actions/upload.ts
"use server";

const API = "https://putput.io/api/v1";
const TOKEN = process.env.PUTPUT_TOKEN!;

export async function presignUpload(filename: string, contentType: string, sizeBytes: number) {
  const res = await fetch(`${API}/upload/presign`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ filename, content_type: contentType, size_bytes: sizeBytes }),
  });
  return res.json();
}

export async function confirmUpload(uploadId: string) {
  const res = await fetch(`${API}/upload/confirm`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ upload_id: uploadId }),
  });
  return res.json();
}

3. Client upload component

// app/components/FileUpload.tsx
"use client";

import { presignUpload, confirmUpload } from "@/app/actions/upload";
import { useState } from "react";

export function FileUpload() {
  const [url, setUrl] = useState<string | null>(null);
  const [uploading, setUploading] = useState(false);

  async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploading(true);

    // 1. Presign
    const presign = await presignUpload(file.name, file.type, 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 confirmUpload(presign.upload_id);
    setUrl(result.file.public_url);
    setUploading(false);
  }

  return (
    <div>
      <input type="file" onChange={handleFile} disabled={uploading} />
      {uploading && <p>Uploading...</p>}
      {url && <a href={url}>{url}</a>}
    </div>
  );
}
v0.4.78