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@latest
This guide uses the automatic options and imageProps() available in SDK 1.4.1. Update an older installation before following these examples.
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,
});
// Preserve delivery URLs and image dimensions for responsive images later.
return NextResponse.json(result.data);
}
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";
import type { FileMetadata } from "getpronto-sdk";
export default function ImageUploader() {
const [file, setFile] = useState<File | null>(null);
const [uploaded, setUploaded] = useState<FileMetadata | 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.secureUrl} alt={uploaded.name} width={600} />
</div>
)}
</div>
);
}
Drop <ImageUploader /> into any page and you have a working image upload.
5. Serve transformed images
Once an image is uploaded, start with its secureUrl and preserve the full path. You can resize, convert formats, and apply effects through URL parameters. These delivery URLs are public and do not require an API key; file management still requires authentication.
Resize to 400px wide:
https://api.getpronto.io/v1/file/your-file.jpg?w=400
Convert to WebP at quality 80:
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
Let Get Pronto pick the format and quality:
https://api.getpronto.io/v1/file/your-file.jpg?w=800&format=auto&q=auto
format=auto lets Get Pronto pick a widely-supported, efficient output format for you.
q=auto picks a compression level for that particular image rather than using a fixed
default. Both return the same bytes to every visitor, so the URL stays cacheable — and
you keep the original file extension in the path.
You can use these directly in your JSX:
// In your component, after upload:
const optimizedUrl = `${uploaded.secureUrl}?w=800&format=auto&q=auto`;
<img src={optimizedUrl} alt={uploaded.name} width={800} />
The first request generates the transformation. Later requests reuse the stored result, and CDN cache hits serve it from the edge. See the format and fallback reference for animation, transparency, SVG/BMP and transformation-quota behaviour.
6. Serve responsive images
A single width still sends desktop-sized images to phones. imageProps builds a
srcset so the browser downloads the size it actually needs. It runs locally, needs
no API key, and makes no network call — safe in a Client Component.
Add this import to ImageUploader.tsx:
import { imageProps } from "getpronto-sdk";
Replace the uploaded result block from step 4 with:
{uploaded && (
<div>
<p>Uploaded: {uploaded.name}</p>
<img
{...imageProps(uploaded, {
widths: [400, 800, 1200],
sizes: "(max-width: 768px) 100vw, 50vw",
format: "auto",
q: "auto",
})}
alt={uploaded.name}
style={{ width: "100%", height: "auto" }}
/>
</div>
)}
You supply sizes because only you know the layout — here, full width on mobile and
half width on wider screens. Match it to the actual container width in your page.
The browser chooses a candidate using that hint and the device's pixel density;
there is no w=auto or h=auto API option.
Step 3 preserves the uploaded metadata, including secureUrl and dimensions when
available. When both dimensions are present, imageProps returns width and height
to reserve space as the image loads. Older uploads may lack dimensions. A bare URL
also works, but it cannot provide dimensions or the source width.
Candidates wider than a known source width are dropped. If all candidates are too
large, the source width is used. Without explicit widths, the helper uses
[400, 800, 1200, 1600]; it accepts at most six unique valid candidates. Get Pronto
does not enlarge the original image.
Each requested width can create a separate transformation, so keep the list short — three or four covers most layouts. Generating the markup alone does not generate all the variants. See the responsive SDK reference for all options and returned attributes.
7. 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<{ secureUrl: 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({ secureUrl: result.data.secureUrl });
}
return (
<div>
<input type="file" accept="image/*" onChange={handleUpload} />
{uploaded && <img src={uploaded.secureUrl} 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
