Quickstart
From a configured S3 bucket to a working Next.js upload in about five minutes.
Before you start you need:
- A Next.js App Router project.
- An S3 bucket with CORS configured for browser
PUTuploads and ACLs allowed, plus AWS credentials in environment variables. - A Prisma client exported from
lib/prisma.ts, with the requiredFilemodel. - Any auth helper that resolves a user id from request headers — the quickstart
calls it
auth(headers).
Only step 3's handler wiring is Next-specific — see Handlers for Bun, Cloudflare, and Express.
You will write four files — an UploadStuff instance, a typed file router, the
route handler, and the client helpers — then drop the upload component into any
page. The finished flow runs in the
kitchen-sink example.
npm install @upload-stuff/server @upload-stuff/client @upload-stuff/react zodpnpm add @upload-stuff/server @upload-stuff/client @upload-stuff/react zodyarn add @upload-stuff/server @upload-stuff/client @upload-stuff/react zodbun add @upload-stuff/server @upload-stuff/client @upload-stuff/react zodThe S3 and Prisma adapters used below also need their peer dependencies:
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @prisma/clientCreate an UploadStuff instance
import { UploadStuff } from "@upload-stuff/server";
import { s3Adapter } from "@upload-stuff/server/adapters/s3";
import { prismaAdapter } from "@upload-stuff/server/adapters/prisma";
import { prisma } from "./prisma";
export const uploadStuff = UploadStuff({
storageAdapter: s3Adapter({
config: {
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
},
bucket: process.env.S3_BUCKET!,
}),
databaseAdapter: prismaAdapter({ prisma }),
filePublicUrlGenerator: ({ key }) =>
`https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`,
});Define a file router
import { z } from "zod";
import { createUploadStuffRouter, UploadStuffError } from "@upload-stuff/server";
import { uploadStuff } from "./upload-stuff";
type Context = { userId: string | null };
const f = createUploadStuffRouter<typeof uploadStuff, Context>();
export const fileRouter = {
avatar: f({
isPublic: true,
files: ["image/*"],
maxFileSize: "4MB",
maxFileCount: 1,
})
.middleware(({ ctx }) => {
if (!ctx.userId) throw new UploadStuffError({ code: "FORBIDDEN", message: "Sign in to upload" });
return { userId: ctx.userId };
})
.onUploadComplete(({ files, middlewareData }) => {
console.log("Uploaded by", middlewareData.userId, files);
})
.build(),
};
export type FileRouter = typeof fileRouter;Auth lives in .middleware(), not in createContext — see the
security model. .onUploadComplete() runs on your
server after S3 confirms the upload; whatever it returns is sent to the
client's onClientUploadComplete, fully typed.
Wire the Next.js route handler
import { toNextJsHandler } from "@upload-stuff/server/next";
import { uploadStuff } from "@/lib/upload-stuff";
import { fileRouter } from "@/lib/file-router";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler({
uploadStuff,
fileRouter,
config: {},
createContext: async ({ headers }) => {
const session = await auth(headers);
return { userId: session?.userId ?? null };
},
});createContext runs for every request, including the public route-config
endpoint — resolve identity gently and never throw for anonymous users. See
the security model.
Create the client helpers and upload
Set NEXT_PUBLIC_APP_URL in .env.local (e.g. http://localhost:3000):
"use client";
import { createUploadStuffClient } from "@upload-stuff/client";
import { createUploadStuffReactHelpers } from "@upload-stuff/react";
import type { FileRouter } from "@/lib/file-router";
const client = createUploadStuffClient<FileRouter>({
baseURL: process.env.NEXT_PUBLIC_APP_URL!,
});
export const { useUploadStuff } = createUploadStuffReactHelpers(client);Passing the server's FileRouter type to createUploadStuffClient types the
client, and createUploadStuffReactHelpers(client) infers the hooks from the
client instance — see Type safety.
"use client";
import { useUploadStuff } from "@/lib/upload-stuff-client";
export function AvatarUpload() {
const { startUpload, isUploading, accept } = useUploadStuff((r) => r.avatar, {
onClientUploadComplete: (res) => console.log("Done:", res.files),
onUploadError: (err) => console.error(err.message),
});
return (
<input
type="file"
accept={accept}
disabled={isUploading}
onChange={(e) => {
// startUpload also rejects on failure; onUploadError already handles it
startUpload(Array.from(e.target.files ?? [])).catch(() => {});
}}
/>
);
}Render <AvatarUpload /> on any page and upload a file.