upload-stuff

File router

Define typed upload routes with file constraints, input schemas, middleware, fields, and completion callbacks.

The file router maps route names to upload routes. Each route is one f(routeConfig)....build() chain, and the map you export is your FileRouter: the type your client consumes. Name routes after what they carry — avatar, resume, messageAttachment.

Defining a route

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,
  })
    .input(z.object({ albumId: z.string() }))
    .middleware(({ ctx }) => {
      if (!ctx.userId) throw new UploadStuffError({ code: "FORBIDDEN", message: "Sign in to upload" });
      return { userId: ctx.userId };
    })
    .fields(({ input }) => ({ albumId: input.albumId }))
    .onUploadComplete(({ files, input, middlewareData }) => {
      console.log("Uploaded to", input.albumId, "by", middlewareData.userId, files);
    })
    .build(),
};

export type FileRouter = typeof fileRouter;

Route config

OptionMeaning
isPublicWhether uploaded files are public.
filesAn array of MIME keys, or a record keyed by MIME key, wildcard, custom MIME type, or blob — record entries can set their own maxFileSize and maxFileCount.
maxFileSizePer-file cap; becomes the default for each type.
maxFileCountTotal number of files allowed for the whole route.

For the full RouteConfig and PerTypeConfig shapes, see the core reference.

The builder chain

  • .input(schema) — validates the client-supplied input with any Standard Schema compatible parser; the parsed output is what later steps receive as input.
  • .middleware(fn) — runs at init only; put auth and context gates here, and its return becomes middlewareData for .fields() and .onUploadComplete().
  • .fields(resolver) — supplies values for custom fields declared on your UploadStuff instance.
  • .onUploadComplete(fn) — runs on your server after storage verification; receives { files, input, middlewareData }, no ctx.
  • .build() — finalizes the route; required for every route in the map.

Custom fields

Declare custom fields on the instance; each route resolves their values with .fields():

export const uploadStuff = UploadStuff({
  // ...
  fields: {
    albumId: { type: "string", required: true },
  },
});
avatar: f({ isPublic: true, files: ["image/*"], maxFileCount: 1 })
  .input(z.object({ albumId: z.string() }))
  .middleware(/* ... */)
  .fields(({ input }) => ({ albumId: input.albumId }))
  // ...

When a declared field is required, omitting .fields() makes .build() resolve to a type error instead of a route.

Gotchas

  • Auth lives in .middleware(), not createContext or isPublic.
  • Middleware runs on init, not on completion.
  • Each builder method is intended to be called once.
  • Order the chain so later steps can see earlier inferred data.

Read the security model before shipping public upload routes.

Next steps

On this page