upload-stuff

Quickstart

From a configured S3 bucket to a working Next.js upload in about five minutes.

Before you start you need:

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 zod
pnpm add @upload-stuff/server @upload-stuff/client @upload-stuff/react zod
yarn add @upload-stuff/server @upload-stuff/client @upload-stuff/react zod
bun add @upload-stuff/server @upload-stuff/client @upload-stuff/react zod

The S3 and Prisma adapters used below also need their peer dependencies:

pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @prisma/client

Create an UploadStuff instance

lib/upload-stuff.ts
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

lib/file-router.ts
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

app/api/upload-stuff/[...uploadStuff]/route.ts
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):

lib/upload-stuff-client.ts
"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.

components/avatar-upload.tsx
"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.

Next steps

On this page