Skip to main content

uploadHandler is the TypeScript server side of a direct browser upload. What the handler does, the order its callbacks run in, and their guarantees are on Upload handler. This page has the TypeScript names, types and framework wiring.

lib/uploads.ts

uploadHandler(options) returns { GET, POST }, a pair of route handlers.


Mounting it#

app/api/upload/route.ts
lib/upload-hooks.ts

That is Next.js App Router. Where the client looks for the handler is on Mounting it; endpoint on uploadHooks or useUpload changes it. uploadHooks<typeof uploads> binds the client to the handler’s type, so route names and completion data are checked at compile time. The import type is erased at build time and never pulls server code into the browser bundle. Upload client has the hooks in full.

Other frameworks#

GET and POST are plain (request: Request) => Promise<Response> functions, so any framework that hands you a fetch Request can mount them.

Frameworks built on Node’s http module, such as Express, need an adapter that converts the incoming request into a fetch Request first.

On Cloudflare Workers the token only exists on the request’s env, so build the bucket from env and pass it as bucket; see Cloudflare Workers.


Options#

lib/uploads.ts
OptionTypeDescription
bucketBucketThe bucket every route writes to. See The bucket.
constraints{ contentTypes?, maxSize? }What the route accepts. See Constraints.
multipartboolean | SizeWhere an upload stops being one PUT. See Large files.
endpointstringWhere the handler is mounted. Only needed to separate two handlers on one bucket.
context(request: Request) => TCtxRuns once per POST. Its value is ctx in every callback. Without it, ctx is undefined. See Context.
inputStandard SchemaValidates what the browser sends as input before onBeforeUpload runs. Every route inherits it; a route declares its own with uploadRoute().
onBeforeUpload(args) => { path, ... }Required, here or on every route. See onBeforeUpload.
onUploadComplete(args) => TDataWhat it returns becomes upload.blob.data. See onUploadComplete.
onError(args) => BlobError | Response | voidSees every refusal. See onError.
routesRecord<string, route>Mounts several routes at this one endpoint. See Routes.

Everything except routes, endpoint and context is a default each route inherits; see Multiple routes. Bad options throw a TypeError at startup, where they are written: an unparseable multipart size, an invalid route name, an empty routes map, a missing token.

The bucket#

lib/uploads.ts

What the handler uses with no bucket is on Handler options. Pass bucket: when the token lives under another variable, the bucket needs cache, or you are on Cloudflare Workers.


onBeforeUpload#

lib/uploads.ts
ArgumentTypeDescription
ctxTCtxWhatever context returned for this request.
routestringThe route this file was sent to. '' when the handler mounts no named routes.
requestRequestThe begin request, headers and cookies intact.
file{ name, type, size }What the browser declared, before a byte was sent.
inputTInputThe validated input, when the route declares a schema.

What it returns:

FieldTypeDescription
pathstringRequired. Where the object is stored.
cacheCacheOptionThe Cache-Control this object is stored with. See Caching.
metadataRecord<string, string>Signed into the upload and handed back to onUploadComplete.
constraints{ contentTypes?, maxSize? }Narrows this one upload’s limits.
stateTStateAnything computed here that onUploadComplete and onError should get without a lookup. Typed from what this callback returns in a handler without routes, written above onUploadComplete. With routes, give each route that needs it an uploadRoute().

What each field does is on Authorizing an upload. Throw a BlobError to refuse:

lib/uploads.ts
Write onBeforeUpload above onUploadComplete
lib/uploads.ts

In a handler without routes, write onBeforeUpload above onUploadComplete, or annotate its parameter. When onBeforeUpload is written below with an unannotated parameter, TypeScript fixes state to undefined first and reports the error on onBeforeUpload: { name: string } is not assignable to undefined.


onUploadComplete#

lib/uploads.ts
ArgumentTypeDescription
pathstringWhere the object is stored.
urlstring | undefinedThe public URL. undefined on a private bucket.
versionedUrlstring | undefinedurl with the etag on the query.
sizenumberBytes actually stored.
etagstringThe stored object’s etag.
uploadedAtDateWhen storage wrote it.
contentTypestringWhat the object is stored as.
ctxTCtxWhat context returned for this request.
routestringThe route name, '' for a sole route.
requestRequestThe end request.
file{ name, type, size }What the browser declared at begin.
uploadIdstringIdentifies this upload. Stable across retries.
multipartUploadIdstring | undefinedFor bucket.abortMultipartUpload(). undefined for a single PUT.
metadataRecord<string, string>What onBeforeUpload returned, minus the SDK’s marker.
stateTStateWhat onBeforeUpload returned as state.

What each field means, and why it may run more than once, is on Recording the upload.

What it returns is handed to the browser as upload.blob.data, typed through uploadHooks<typeof uploads>:

app/page.tsx

onError#

lib/uploads.ts
ArgumentTypeDescription
ctxTCtx | undefinedundefined when context itself threw, or when no route matched.
routestringThe name from the query, even when nothing mounts it.
requestRequest
errorunknownWhatever was thrown.
file, path, metadata, stateoptionalAs much as the request had reached before it failed.

Return a BlobError or a Response to answer with it, or nothing to leave the answer alone. See Handling errors.


context#

lib/uploads.ts

context takes the POST’s Request. Its awaited value is ctx in every callback, and typed there. When it runs is on Context.

Write context above routes
lib/uploads.ts

Write context above routes, or annotate its parameter as (request: Request) =>. Written below routes with an unannotated parameter, TypeScript infers ctx: undefined for the routes first and reports the error on context: Promise<Session> is not assignable to undefined.

Callbacks in another file#

For a callback written in another file, annotate the argument with the helper types, which take the handler and produce the right shape:

lib/callbacks.ts

Routes#

lib/uploads.ts
app/page.tsx

A route is an object with any of bucket, constraints, multipart, onBeforeUpload, onUploadComplete and onError. A key it names overrides the handler’s. A handler with no routes is reached by a bound useUpload() that takes no argument. The rules for names and inheritance are on Multiple routes.


uploadRoute()#

lib/uploads.ts
app/page.tsx

Use uploadRoute() when a route needs an input schema for data the browser sends along with the file, or a typed state passed from onBeforeUpload to onUploadComplete. A plain route object cannot type either. A handler without routes types state from its own onBeforeUpload, written above its onUploadComplete; with routes, each route that needs it takes an uploadRoute(). It is curried so the ctx type can be named, and it is written outside the routes map.

  • input takes any Standard Schema, such as zod. Only the parsed value reaches onBeforeUpload.
  • Everything else on the route works as on a plain object: bucket, constraints, multipart, onError, and the same inheritance from the handler.

How input is validated and what state exposes are on Input and state.