Skip to main content

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:

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_KEY to 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=auto or a named preset.

Last updated at: May 8, 2026