Tanstack Start

Framework integrations

Mount the route handler on Next.js, Hono, and any Web Request runtime.

createRouteHandler returns a plain (request: Request) => Promise<Response>. Any framework that gives you a Web Request and lets you return a Response can mount it as-is. Below are the common ones.

In every case the handler holds your S3Delivery client (and your secret key), and the browser uploader points its endpoint at the mounted path (default /api/s3delivery).

Next.js (App Router)

Export the handler as the POST of a route segment.

// app/api/s3delivery/route.ts
import { S3Delivery, createRouteHandler } from "s3delivery";

export const POST = createRouteHandler(new S3Delivery(), {
  constraints: { maxFileSize: 10 * 1024 * 1024, allowedFileTypes: ["image/*"] },
});

The browser uploads to /api/s3delivery by default, so no endpoint is needed. If you mount it elsewhere, set endpoint on the uploader:

useUploadFiles({ endpoint: "/api/uploads" });

Next.js (Pages Router)

The handler speaks the Web Request/Response API, but Pages Router API routes use Node req/res. Prefer an App Router route segment as above. If you must use the Pages Router, adapt the body yourself with createUpload / complete on the server client instead of createRouteHandler.

Hono

Pass the raw request through and return the response:

import { Hono } from "hono";
import { S3Delivery, createRouteHandler } from "s3delivery";

const app = new Hono();
const handler = createRouteHandler(new S3Delivery());

app.post("/api/s3delivery", (c) => handler(c.req.raw));

Remix

Use the handler in an action.

// app/routes/api.s3delivery.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { S3Delivery, createRouteHandler } from "s3delivery";

const handler = createRouteHandler(new S3Delivery());

export const action = ({ request }: ActionFunctionArgs) => handler(request);

SvelteKit

// src/routes/api/s3delivery/+server.ts
import { S3Delivery, createRouteHandler } from "s3delivery";

const handler = createRouteHandler(new S3Delivery());

export const POST = ({ request }) => handler(request);

Cloudflare Workers / Bun / Deno

These runtimes hand you a Request and want a Response, so the handler is the whole fetch handler — or one branch of it.

// Cloudflare Workers
import { S3Delivery, createRouteHandler } from "s3delivery";

const handler = createRouteHandler(new S3Delivery());

export default {
  async fetch(request: Request): Promise<Response> {
    const { pathname } = new URL(request.url);
    if (pathname === "/api/s3delivery") return handler(request);
    return new Response("Not found", { status: 404 });
  },
};
// Bun
import { S3Delivery, createRouteHandler } from "s3delivery";

const handler = createRouteHandler(new S3Delivery());

Bun.serve({
  fetch(request) {
    const { pathname } = new URL(request.url);
    if (pathname === "/api/s3delivery") return handler(request);
    return new Response("Not found", { status: 404 });
  },
});

Express (and other Node frameworks)

Express uses Node's req/res, not Web Request/Response. Either upgrade to a Web-standard adapter, or skip createRouteHandler and broker the handshake with the server client directly:

import express from "express";
import { S3Delivery } from "s3delivery";

const app = express();
const s3 = new S3Delivery();

app.use(express.json());

app.post("/api/s3delivery", async (req, res) => {
  try {
    if (req.body.action === "create") {
      res.json(await s3.createUpload(req.body));
    } else if (req.body.action === "complete") {
      res.json(
        await s3.complete(req.body.fileId, {
          uploadId: req.body.uploadId,
          parts: req.body.parts,
        }),
      );
    } else {
      res.status(400).json({ error: { code: "BAD_REQUEST", message: "Unknown action" } });
    }
  } catch (e) {
    res.status(500).json({ error: { code: "INTERNAL", message: "Internal error" } });
  }
});

This matches the body shape the browser uploader sends ({ action, … }), so the client SDK works against it unchanged. Add your own auth and constraint checks before createUpload.

Server-only, no browser

If you only ever upload from the server (generated files, agent output, fetched assets), you do not need a route handler at all — just use s3.upload(...) directly.

On this page