Next.js
App Router integration for the Silo upload router.
@silo-storage/sdk-next is the App Router adapter for the Silo SDK stack.
Use it when you already have a typed upload router from @silo-storage/sdk-server and want to expose it through a Next.js route.ts handler.
What it provides
createRouteHandler(...)forGETandPOSTroute exports- callback verification and
onUploadCompletedispatch - registration handling for browser uploads
- completion polling used by the React client
extractRouterConfig(...)for optional client hydration
Getting started
Install the package
npm install @silo-storage/sdk-next @silo-storage/sdk-react @silo-storage/sdk-server @silo-storage/sdk-coreDefine your file router
import type { FileRouter } from "@silo-storage/sdk-server";
import { createSiloUpload } from "@silo-storage/sdk-server";
import { z } from "zod";
export interface UploadContext {
userId: string; // you can have whatever you want here
};
const f = createSiloUpload<Request, UploadContext>();
export const fileRouter = {
imageUploader: f(
z.object({
folder: z.enum(["avatars", "attachments"]).default("avatars"),
kind: z.enum(["image", "binary"]).default("image"),
}),
)
.middleware(async ({ context, input }) => {
if (!context?.userId) {
throw new Error("Unauthorized");
}
return {
userId: context.userId,
folder: input.folder,
};
})
.expects(({ input }) =>
input.kind === "binary"
? [
{
mimeTypes: ["application/xyz", "application/abc"],
maxFileCount: 4,
maxFileSize: "16MB",
},
]
: {
image: {
maxFileSize: "8MB",
maxFileCount: 4,
mimeTypes: ["image/png", "image/jpeg"],
},
},
)
.public(false) // do we need a signed url to access?
.serveImage(({ input }) => input.folder === "avatars") // serve images from the image CDN (transformations etc)
.expires("30 days") // delete after 30 days
.onUploadComplete(async ({ metadata, file }) => {
return {
uploadedBy: metadata.userId,
fileKeyId: file.fileKeyId,
};
}),
} satisfies FileRouter<Request, UploadContext>;
export type AppFileRouter = typeof fileRouter;If you want more detail on route definitions, middleware, or callback behavior, read Server.
Mount the App Router handler
Create a route handler in your Next.js app, usually at app/api/upload/route.ts.
import { auth } from "your-auth-lib/server"; // this can be anything
import { createSiloCoreFromToken } from "@silo-storage/sdk-core";
import { createRouteHandler } from "@silo-storage/sdk-next";
import type { UploadContext } from "@/upload";
import { fileRouter } from "@/upload";
const core = createSiloCoreFromToken({
url: process.env.SILO_URL!,
token: process.env.SILO_TOKEN!,
cdnHost: process.env.NEXT_PUBLIC_SILO_CDN!,
});
export const { GET, POST } = createRouteHandler<UploadContext>({
router: fileRouter,
core,
// Defaults to "sse" in development and "callback-url" otherwise.
// Override it when you want to force one transport explicitly.
completionTransport: "auto",
resolveContext: async (request: Request): Promise<UploadContext> => {
// do whatever you need here to satisfy the UploadContext type
const { userId } = await auth(request);
if (!userId) throw new Error("Unauthorized");
return { userId };
},
});This matches Next.js App Router route handlers, where route.ts exports HTTP method functions such as GET and POST.
What the route handler does
createRouteHandler(...) returns two handlers:
GETreturns the router config so clients can discover route metadata safelyPOSThandles upload registration requests from the browserPOSTalso verifies signed Silo callbacks and dispatches the matchingonUploadCompletePOSThandles completion polling used by the React SDK after an upload finishes
By default, the adapter uses completionTransport: "auto", which selects sse when NODE_ENV === "development" and callback-url otherwise.
When the adapter uses callback URLs, it defaults to the current route URL. Override that with callbackUrl if your public callback origin differs from the incoming request origin.
Set completionTransport explicitly when you want to force one mode:
"auto"to keep the environment-based default"sse"to always use the development SSE flow"callback-url"to always use signed callbacks
Using in conjunction with the React SDK
When using @silo-storage/sdk-react, you can hydrate the router config on the server so the client does not need to fetch it on first render.
"use client";
import { createSiloReact } from "@silo-storage/sdk-react";
import type { AppFileRouter } from "@/upload";
export const {
useUpload,
useStagedUpload,
UploadButton,
UploadDropzone,
SiloRouterConfigProvider,
} = createSiloReact<AppFileRouter>({
endpoint: "/api/upload",
});import { extractRouterConfig } from "@silo-storage/sdk-next";
import { SiloRouterConfigProvider } from "@/lib/upload";
import { fileRouter } from "@/upload";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<SiloRouterConfigProvider routerConfig={extractRouterConfig(fileRouter)}>
{children}
</SiloRouterConfigProvider>
</body>
</html>
);
}For the client-side hooks and headless components themselves, continue with React.
If a route uses a function-based .expects(...) resolver, extractRouterConfig(fileRouter) cannot expose a static picker filter for that route. In that case, pass accept to the React hook or component that opens the file picker.
Completion storage
The Next adapter stores onUploadComplete results temporarily so the client can wait for completion after the upload finishes.
By default it will:
- use an HTTP-backed completion store when it has a base API URL and auth token
- fall back to an in-memory store otherwise
You can override that behavior with:
completionStorecompletionStoreUrlcompletionStoreAuthTokencompletionStorePathPrefixcompletionTtlMs
Use a shared store in production when the upload callback and the polling request may run on different instances. For the full flow, the store contract, and replacement examples including Redis, read Completion Store.
Notes
- Callback verification depends on
core.config.signingSecret.createSiloCoreFromToken(...)fills that fromSILO_TOKEN, so you usually do not need to pass anything extra intocreateRouteHandler(...). - If you force
completionTransport: "callback-url"in development, make sure the configured callback endpoint is publicly reachable by Silo. extractRouterConfig(...)only exposes the client-safe subset of your router definition for routes with statically known expectations.
API reference
For generated type tables covering the route handler and completion store options, see API Reference.