Next.js App Router
Complete setup guide for Next.js App Router with @uploadkitdev/next and @uploadkitdev/react.
Prerequisites
- Next.js 14 or later (App Router)
- React 18 or later
- pnpm, npm, or yarn
Installation
Install both @uploadkitdev/next for the server-side handler and @uploadkitdev/react for UI components:
pnpm add @uploadkitdev/next @uploadkitdev/reactnpm install @uploadkitdev/next @uploadkitdev/reactyarn add @uploadkitdev/next @uploadkitdev/reactCreate the file router
Create a Route Handler at app/api/uploadkit/route.ts. This is where you define every upload category your app supports, along with its constraints and server-side logic.
import { createUploadKitHandler } from '@uploadkitdev/next';
import type { FileRouter } from '@uploadkitdev/next';
import { auth } from '@/lib/auth'; // your Auth.js or Clerk auth helper
const router = {
// Accept profile images up to 4 MB
imageUploader: {
maxFileSize: '4MB',
allowedTypes: ['image/*'],
onUploadComplete: async ({ file, metadata }) => {
console.log('Image uploaded by user:', metadata.userId, file.url);
// Save to your database here
},
},
// Accept PDF / Word documents up to 16 MB, max 5 files at once
documentUploader: {
maxFileSize: '16MB',
maxFileCount: 5,
allowedTypes: ['application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
middleware: async ({ req }) => {
// Authenticate the request — runs on your server before the upload is authorised
const session = await auth();
if (!session?.user?.id) {
throw new Error('Unauthorized');
}
// Anything returned here is passed to onUploadComplete as `metadata`
return { userId: session.user.id, plan: session.user.plan };
},
onUploadComplete: async ({ file, metadata }) => {
// metadata.userId and metadata.plan are fully typed
console.log('Document uploaded:', file.key, 'by', metadata.userId);
},
},
// Square avatar images only — 2 MB limit
avatarUploader: {
maxFileSize: '2MB',
maxFileCount: 1,
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
middleware: async ({ req }) => {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
return { userId: session.user.id };
},
onUploadComplete: async ({ file, metadata }) => {
// Update user avatar URL in your database
await db.user.update({ where: { id: metadata.userId }, data: { avatarUrl: file.url } });
},
},
} satisfies FileRouter;
export const { GET, POST } = createUploadKitHandler({ router });The satisfies FileRouter pattern (not a type annotation) preserves the literal route name strings. This is what enables end-to-end TypeScript on the client — the route prop is typed to "imageUploader" | "documentUploader" | "avatarUploader".
Environment variables
Add your API key to .env.local:
# Server-side only — used by createUploadKitHandler to authenticate with the UploadKit API
UPLOADKIT_API_KEY=uk_live_xxxxxxxxxxxxxxxxxxxxxNever prefix your UploadKit API key with NEXT_PUBLIC_. It belongs on the server only. If using BYOS, your storage credentials go here too — server-side only.
Add the provider
Wrap your root layout's children with UploadKitProvider, pointing it at the route handler you just created. The provider calls your local /api/uploadkit endpoint — your API key stays server-side and never reaches the browser:
import { UploadKitProvider } from '@uploadkitdev/react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<UploadKitProvider endpoint="/api/uploadkit">
{children}
</UploadKitProvider>
</body>
</html>
);
}Use components
UploadKit ships three ready-made upload components. Use whichever fits your UI:
UploadButton
A single button that opens the system file picker:
import { UploadButton } from '@uploadkitdev/react';
export default function ProfilePage() {
return (
<UploadButton
route="imageUploader"
onUploadComplete={(files) => {
console.log('Files uploaded:', files.map((f) => f.url));
}}
onUploadError={(error) => {
alert(`Upload failed: ${error.message}`);
}}
/>
);
}UploadDropzone
A drag-and-drop zone with a file picker fallback:
import { UploadDropzone } from '@uploadkitdev/react';
export default function DocumentsPage() {
return (
<UploadDropzone
route="documentUploader"
onUploadComplete={(files) => {
console.log('Documents uploaded:', files);
}}
/>
);
}UploadModal
An accessible modal dialog triggered by a button:
'use client';
import { useState } from 'react';
import { UploadModal } from '@uploadkitdev/react';
export default function SettingsPage() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Change avatar</button>
<UploadModal
open={open}
onOpenChange={setOpen}
route="avatarUploader"
onUploadComplete={(files) => {
setOpen(false);
console.log('Avatar updated:', files[0]?.url);
}}
/>
</>
);
}Handle upload completion
The onUploadComplete callback in your file router runs server-side after the file has been stored. Use it to update your database, trigger webhooks, or send notifications:
documentUploader: {
// ...config...
onUploadComplete: async ({ file, metadata }) => {
// file: { id, key, name, size, type, url, status, createdAt }
// metadata: whatever your middleware returned
// Save the file record to your database
await db.document.create({
data: {
userId: metadata.userId,
fileKey: file.key,
fileName: file.name,
fileUrl: file.url,
fileSize: file.size,
},
});
// Optionally trigger downstream processing
await queueDocumentProcessing({ fileKey: file.key });
},
},For real-time updates in the client after upload completes, listen to the onUploadComplete callback on the component and update local state or revalidate with router.refresh().
Type safety
Use generateReactHelpers to get a fully-typed version of the upload components bound to your specific router:
import { generateReactHelpers } from '@uploadkitdev/react';
import type { FileRouter } from '@/app/api/uploadkit/route';
// Re-export typed components — route prop is now typed to your route names
export const { UploadButton, UploadDropzone, UploadModal, useUploadKit } =
generateReactHelpers<FileRouter>();Then import these typed versions throughout your app:
import { UploadButton } from '@/lib/uploadkit';
// route prop is now typed: "imageUploader" | "documentUploader" | "avatarUploader"BYOS mode
If you want to use your own S3 or Cloudflare R2 bucket instead of UploadKit's managed storage, pass a storage configuration to createUploadKitHandler. Your frontend code stays identical.
See the Bring Your Own Storage guide for the full setup.