Next.js Direct Uploads
Prepare signed direct uploads in a Next.js route handler and send files from the browser with FormData.
This guide shows a Next.js App Router upload flow:
- A browser sends file information to a route handler.
- The route handler authenticates the request and calls Upload SDK.
- The browser sends the file directly to AWS S3 or ImageKit.
Provider credentials stay on the server. The file itself does not pass through the Next.js route handler.
Server-only uploader
Create the uploader in server-only code:
// src/server/upload.ts
import {
createUploader,
defineAssets,
defineStorageProfiles,
} from "@marinedotsh/upload-sdk";
import { awsS3 } from "@marinedotsh/upload-sdk/providers";
const storageProfiles = defineStorageProfiles({
uploads: awsS3({
bucket: process.env.AWS_BUCKET!,
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
});
const assets = defineAssets(storageProfiles, {
avatar: {
keyPrefix: "avatars",
accept: {
mimeTypes: ["image/png", "image/jpeg"],
extensions: [".png", ".jpg", ".jpeg"],
},
limits: {
maxFileSize: {
value: 2,
unit: "MB",
},
},
expiresIn: {
value: 5,
unit: "minutes",
},
},
});
export const uploader = createUploader({
storageProfiles,
defaultStorageProfile: "uploads",
assets,
});Keep this file out of client components. It reads provider credentials and should only run on the server.
Route handler
Create a route handler that accepts file information, not the file bytes:
// src/app/api/uploads/avatar/route.ts
import { NextResponse } from "next/server";
import { uploader } from "@/server/upload";
export async function POST(request: Request) {
// Authenticate and authorize the request here.
const input = await request.json();
const preparedUpload = await uploader.prepareUpload("avatar", {
filename: input.filename,
contentType: input.contentType,
size: input.size,
});
return NextResponse.json(preparedUpload);
}Your application should decide which asset the user may upload to. Do not let the browser choose arbitrary assets, buckets, key prefixes, or storage profiles.
Browser request
In a client component or browser utility, request the prepared upload:
import type { PrepareUploadOutput } from "@marinedotsh/upload-sdk";
async function prepareAvatarUpload(file: File): Promise<PrepareUploadOutput> {
const response = await fetch("/api/uploads/avatar", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
}),
});
if (!response.ok) {
throw new Error("Could not prepare the upload.");
}
return response.json();
}The response contains the provider URL, signed fields, method, headers, generated storage key, and expiration time.
Send the file
Use the prepared upload to build a multipart form:
async function uploadFile(
file: File,
preparedUpload: PrepareUploadOutput,
): Promise<string> {
const formData = new FormData();
for (const [name, value] of Object.entries(preparedUpload.fields)) {
formData.append(name, value);
}
formData.append("file", file);
const response = await fetch(preparedUpload.url, {
method: preparedUpload.method,
headers: preparedUpload.headers,
body: formData,
});
if (!response.ok) {
throw new Error("The provider rejected the upload.");
}
return preparedUpload.key;
}Do not set the multipart Content-Type header manually. The browser adds the required boundary.
Complete client flow
export async function uploadAvatar(file: File): Promise<string> {
const preparedUpload = await prepareAvatarUpload(file);
return uploadFile(file, preparedUpload);
}The returned key is the intended storage key. It does not prove the provider accepted the upload until the direct upload request succeeds.
Next.js responsibilities
Your Next.js application still owns:
- Authentication and authorization
- Choosing the upload asset
- Rate limits and request limits
- Saving trusted upload records
- Confirming upload completion when required
- Inspecting uploaded files when content trust matters
Upload SDK prepares the signed target. It does not persist records or verify stored files.
Next steps
-
Browser Uploads: Understand the provider-independent browser flow.
-
S3 Presigned Browser Uploads: Configure S3-specific CORS, policy, and upload behavior.
-
Direct Upload Architecture: Decide when direct uploads fit your application.