Route handler
Broker browser uploads while your API key stays on the server.
createRouteHandler builds a small upload broker you mount on your server. The
browser SDK talks to it for the create/complete handshake, so your secret API key
never reaches the client. The file bytes still go straight to the bucket — the
handler only signs and authorizes.
// app/api/s3delivery/route.ts (Next.js App Router)
import { S3Delivery, createRouteHandler } from "s3delivery";
export const POST = createRouteHandler(new S3Delivery(), {
constraints: { maxFileSize: 10 * 1024 * 1024, allowedFileTypes: ["image/*"] },
});Signature
createRouteHandler(
client: S3Delivery,
options?: {
constraints?: { maxFileSize?: number; allowedFileTypes?: string[] };
beforeUpload?: (ctx: { request: Request; input: CreateUploadInput })
=> void | Partial<CreateUploadInput> | Promise<void | Partial<CreateUploadInput>>;
onUploadComplete?: (ctx: { request: Request; file: S3DeliveryFile })
=> void | Promise<void>;
},
): (request: Request) => Promise<Response>;It returns a framework-agnostic (Request) => Promise<Response>. Mount it on any
runtime that speaks the Web Request/Response API — Next.js App Router, Hono,
Remix, SvelteKit, Bun, Deno, or Cloudflare Workers. See
Framework integrations.
The handler accepts POST only; other methods get a 405. The browser sends two
kinds of body — { action: "create", … } and { action: "complete", … } — which
the SDK manages for you.
constraints
Per-route limits enforced server-side, before any upload URL is minted. This is the authoritative check — a client can mirror the same values for instant feedback, but cannot skip these.
createRouteHandler(s3, {
constraints: {
maxFileSize: 10 * 1024 * 1024, // bytes
allowedFileTypes: ["image/*", "application/pdf"],
},
});| Field | Effect |
|---|---|
maxFileSize | Rejects an upload whose size exceeds this, with FILE_TOO_LARGE (413). |
allowedFileTypes | Rejects a content type not in the list, with TYPE_NOT_ALLOWED (415). Supports exact types ("application/pdf") and wildcard subtypes ("image/*"); matching is case-insensitive. |
maxFiles is client-only
The handler signs one file at a time, so a maxFiles count is meaningless here and is ignored.
Set maxFiles in the browser upload constraints instead.
beforeUpload
Runs before an upload URL is minted — your place to authenticate the request and constrain the upload. Unlike a webhook, it runs synchronously inside your own server.
- Throw to reject the upload. The thrown error becomes the response.
- Return a partial
CreateUploadInputto override fields — for example, forcevisibility, or clamp the filename. - Return nothing to accept the input as-is.
createRouteHandler(s3, {
beforeUpload: async ({ request, input }) => {
const user = await getUser(request);
if (!user) throw new Error("Unauthorized");
// Force every upload from this route to be private, namespaced by user.
return { visibility: "private", filename: `${user.id}/${input.filename}` };
},
});The ctx.input is { filename, contentType?, size, visibility? }. Any override
you return is merged over it, and constraints are checked against the merged
result.
onUploadComplete
Runs inline when an upload is confirmed complete. Because it is a normal request to your own server, there is no public callback URL or dev tunnel to set up — a common pain point with webhook-based upload SDKs.
createRouteHandler(s3, {
onUploadComplete: async ({ request, file }) => {
const user = await getUser(request);
await db.files.insert({
id: file.id,
ownerId: user.id,
url: file.url,
name: file.name,
size: file.size,
});
},
});The file is the fully-confirmed S3DeliveryFile
(status ready), so this is the right place to persist it to your database, kick
off processing, or send a notification.
How errors surface
The handler catches S3DeliveryError and returns it as JSON with
the matching HTTP status:
{ "error": { "code": "FILE_TOO_LARGE", "message": "…" } }The browser SDK re-throws these as an S3DeliveryError with the same code, so
you can branch on error.code on the client. Unknown errors return a generic
500 with code INTERNAL.
Adding auth headers from the browser
If your route handler authenticates the request (recommended), the browser needs
to send credentials. Pass headers to the uploader:
import { useUploadFiles } from "s3delivery/react";
const { upload } = useUploadFiles({
headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
});headers accepts a static object or a function returning one (sync or async), so
you can refresh a token per upload. See Browser uploads.