Add Image Hosting to Your Next.js App in 5 Minutes
This guide walks you through adding image uploads, hosting, and on-the-fly transformations to a Next.js application. By the end, you'll have a working upload flow and be serving optimized images through a global CDN.
We'll use the Next.js App Router with TypeScript and the official getpronto-sdk.
Prerequisites
- A Next.js app (14+ with App Router)
- A Get Pronto account (free tier works)
- An API key from your Get Pronto dashboard
1. Install the SDK
npm install getpronto-sdk
2. Set up your environment variable
Add your API key to .env.local:
GETPRONTO_API_KEY=your_api_key_here
Your API key should only be used server-side. Never expose it to the browser.
3. Create an upload API route
Create a server-side route that handles file uploads. The SDK accepts Buffer, so we read the incoming file and pass it through.
Create app/api/upload/route.ts:
import { NextRequest, NextResponse } from "next/server";
import GetProntoClient from "getpronto-sdk";
const client = new GetProntoClient({
apiKey: process.env.GETPRONTO_API_KEY!,
});
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const buffer = Buffer.from(await file.arrayBuffer());
const result = await client.files.upload(buffer, {
filename: file.name,
mimeType: file.type,
});
return NextResponse.json({
id: result.data.id,
url: result.data.secureUrl,
name: result.data.name,
});
}
That's it for the backend. The SDK handles the presigned URL flow, uploads directly to storage, and returns the file metadata.
4. Build the upload component
Create a client component that lets users pick a file, uploads it to your API route, and displays the result.
Create app/components/ImageUploader.tsx:
"use client";
import { useState } from "react";
type UploadedFile = {
id: string;
url: string;
name: string;
};
export default function ImageUploader() {
const [file, setFile] = useState<File | null>(null);
const [uploaded, setUploaded] = useState<UploadedFile | null>(null);
const [uploading, setUploading] = useState(false);
async function handleUpload() {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await response.json();
setUploaded(data);
setUploading(false);
}
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
<button onClick={handleUpload} disabled={!file || uploading}>
{uploading ? "Uploading..." : "Upload"}
</button>
{uploaded && (
<div>
<p>Uploaded: {uploaded.name}</p>
<img src={uploaded.url} alt={uploaded.name} width={600} />
</div>
)}
</div>
);
}
Drop <ImageUploader /> into any page and you have a working image upload.
5. Serve transformed images
The real power of Get Pronto is on-the-fly transformations. Once an image is uploaded, you can resize, convert formats, and apply effects just by adding query parameters to the URL.
Resize to 400px wide:
https://api.getpronto.io/v1/file/your-file.jpg?w=400
Convert to WebP at 80% quality:
https://api.getpronto.io/v1/file/your-file.webp?q=80
Resize, convert, and add blur:
https://api.getpronto.io/v1/file/your-file.webp?w=800&q=85&blur=5
You can use these directly in your JSX:
// In your component, after upload:
const optimizedUrl = `${uploaded.url.replace(/\.\w+$/, ".webp")}?w=800&q=85`;
<img src={optimizedUrl} alt={uploaded.name} width={800} />
The first request triggers the transformation. Get Pronto caches the result on a global CDN, so every subsequent request is served from the edge.
6. Generate transform URLs with the SDK
For more complex transformations, use the SDK's fluent transform API server-side:
// In a Server Component or API route
const transformedUrl = await client.images
.transform(fileId)
.resize(800, 600, "cover")
.format("webp")
.quality(85)
.toURL();
This generates a cached transform URL that you can pass to your frontend.
Alternative: Direct browser upload with a public key
If you don't want to proxy uploads through your server, you can use a public API key (pronto_pk_) directly in the browser. Public keys can only upload files — they can't list, read, or delete anything.
Create a public key in your dashboard, then use it directly in a client component:
"use client";
import { useState } from "react";
import GetProntoClient from "getpronto-sdk";
// Public key — safe to use in browser code
const client = new GetProntoClient({
apiKey: "pronto_pk_...",
});
export default function DirectUploader() {
const [uploaded, setUploaded] = useState<{ url: string } | null>(null);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const result = await client.files.upload(file);
setUploaded({ url: result.data.secureUrl });
}
return (
<div>
<input type="file" accept="image/*" onChange={handleUpload} />
{uploaded && <img src={uploaded.url} alt="Uploaded" width={600} />}
</div>
);
}
This removes the need for the API route in step 3 — files upload directly from the browser to storage.
What you get
With this setup, your Next.js app now has:
- File uploads via presigned URLs (fast, no server bottleneck)
- Global CDN delivery for every uploaded file
- On-the-fly image transformations — resize, crop, format conversion, blur, grayscale, rotation, and more
- Automatic caching — transformed images are generated once and served from the edge
Next steps
- SDK documentation — full API reference for uploads, transforms, and file management
- Image transforms reference — all available transformation parameters
- Responsive Images Done Right — combine Get Pronto transforms with
srcsetand<picture>for optimal delivery - Reduce Page Size by 50% with WebP and AVIF — why modern formats matter and how to serve them
