Building a Secure Upload Form in Next.js with Imgwire
This tutorial combines a signed upload-token endpoint with a Next.js client component so users can upload images without exposing a Server API Key.
Use this pattern when uploads should depend on your app's authentication, membership, or server-side permission checks.
Prerequisites
Before building the form:
- Complete the setup guide.
- Create an Imgwire Client Key for frontend uploads.
- Create an Imgwire Server API Key for your Next.js backend.
- Build the Next.js upload token endpoint.
Install the browser SDK:
yarn add @imgwire/js
Set environment variables:
IMGWIRE_API_KEY=sk_...
NEXT_PUBLIC_IMGWIRE_CLIENT_KEY=ck_...
Create the browser client
Create src/lib/imgwire-browser.ts:
import { ImgwireClient } from '@imgwire/js';
export const imgwireBrowser = new ImgwireClient({
apiKey: process.env.NEXT_PUBLIC_IMGWIRE_CLIENT_KEY!,
getUploadToken: async () => {
const response = await fetch('/api/imgwire/upload-token', {
method: 'POST',
});
if (!response.ok) {
throw new Error('Unable to create Imgwire upload token');
}
const { uploadToken } = await response.json();
return uploadToken;
},
});
The Client Key is publishable. The Server API Key stays in the upload-token route and is never sent to the browser.
Build the upload form
Create src/components/secure-image-upload-form.tsx:
'use client';
import { FormEvent, useRef, useState } from 'react';
import { imgwireBrowser } from '@/lib/imgwire-browser';
type UploadedImage = {
id: string;
url: string;
};
export function SecureImageUploadForm() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [image, setImage] = useState<UploadedImage | null>(null);
const [percent, setPercent] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const file = fileInputRef.current?.files?.[0];
if (!file) {
setError('Choose an image before uploading.');
return;
}
if (!file.type.startsWith('image/')) {
setError('Choose a supported image file.');
return;
}
setIsUploading(true);
setError(null);
setPercent(0);
try {
const uploaded = await imgwireBrowser.images.upload(file, {
purpose: 'user upload',
customMetadata: {
uploaded_from: 'nextjs-secure-upload-form',
},
onProgress(progress) {
setPercent(progress.percent);
},
});
setImage({
id: uploaded.id,
url: uploaded.url({
preset: 'medium',
format: 'auto',
quality: 'auto',
}),
});
} catch {
setError('Upload failed. Please try again.');
} finally {
setIsUploading(false);
}
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="image">Image</label>
<input
ref={fileInputRef}
id="image"
name="image"
type="file"
accept="image/*"
disabled={isUploading}
/>
<button type="submit" disabled={isUploading}>
{isUploading ? 'Uploading...' : 'Upload image'}
</button>
{percent !== null ? <p>{Math.round(percent)}% uploaded</p> : null}
{error ? <p role="alert">{error}</p> : null}
{image ? (
<figure>
<img src={image.url} alt="Uploaded preview" />
<figcaption>Imgwire image ID: {image.id}</figcaption>
</figure>
) : null}
</form>
);
}
This form validates basic client-side input, requests an upload token through the SDK, uploads directly to Imgwire, and renders a transformed CDN preview.
Persist the image
After upload, store the Imgwire image ID with the application record that owns the image:
await fetch('/api/profile/avatar', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageId: image.id,
}),
});
Storing the image ID gives your app flexibility. You can later generate @thumbnail, @medium, social, or custom transformed URLs from the same original upload.
Configure your Client Key
In the Imgwire dashboard, configure the Client Key used by this form:
- Disable unsigned uploads if every upload should require a backend-issued token.
- Set the maximum upload size for the UI.
- Restrict supported image MIME types to what your app accepts.
- Add CORS Origins for the browser origins that should be allowed to upload.
Client-side validation improves the user experience, but the key settings and signed upload flow are the controls that matter after the request leaves the browser.
Best practices
- Keep the upload-token route behind your app's auth.
- Never expose
IMGWIRE_API_KEYto client components. - Show upload progress so users do not retry large uploads unnecessarily.
- Store image IDs, not only rendered URLs, when your app needs future variants.
- Render previews with
format=auto&quality=autoor a named preset.
Related pages
- Building a Next.js API endpoint to issue upload tokens with Imgwire
- Frontend Quickstart
- Avatar Upload Form in React
- CORS Origins
Last updated at: May 8, 2026