Add Image Hosting to Your Next.js App in 5 Minutes

By Get Pronto

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

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:

Next steps

Tags

ImagesUploadsTransformations