Skip to content

Next.js Tutorial

Learn how to integrate Icefiery into your Next.js application for optimized image uploads and transformations.

Overview

This tutorial shows you how to:

  • Upload images from Next.js client components
  • Create API routes to handle image saving
  • Optimize image serving with Next.js Image component
  • Handle image transformations server-side

Prerequisites

  • A Next.js application (App Router or Pages Router)
  • API keys from your Icefiery dashboard
  • Environment variables configured

Step 1: Environment Setup

Add your API keys to your environment variables:

bash
# .env.local
ICEFIERY_API_KEY=<your_api_key>
NEXT_PUBLIC_ICEFIERY_UPLOAD_URL=https://api.icefiery.com/api/v1/upload-temporary-image/<your_upload_key>

Or if you're using Docker:

bash
# .env.local
ICEFIERY_API_KEY=local
NEXT_PUBLIC_ICEFIERY_UPLOAD_URL=http://localhost:5100/api/v1/upload-temporary-image/local

Step 2: Create ImageInput Component

INFO

Not mandatory, but this component is a good base for your customizations. It includes:

  • Loading state
  • Error state
  • Preview of uploaded image

Create a simple reusable ImageInput component:

tsx
"use client";

import { useState } from "react";

interface ImageInputProps {
  uploadUrl: string;
  onImageUploaded: (url: string) => void;
  disabled?: boolean;
}

export const ImageInput: React.FC<ImageInputProps> = ({
  uploadUrl,
  onImageUploaded,
  disabled,
}) => {
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleFileUpload = async (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    const file = event.target.files?.[0];
    if (!file) return;

    try {
      setIsLoading(true);
      setError(null);

      const res = await fetch(uploadUrl, {
        method: "POST",
        headers: {
          "X-Original-Filename": file.name,
        },
        body: file,
      });

      const data = await res.json();
      const { temporaryImageUrl } = data;

      onImageUploaded(temporaryImageUrl);
      setPreviewUrl(temporaryImageUrl);
    } catch (error) {
      console.error("Upload failed:", error);
      setError("Uploading image failed");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <>
      <input
        type="file"
        accept="image/jpeg,image/png,image/webp,image/gif,image/avif,image/tiff,image/heic"
        onChange={handleFileUpload}
        disabled={isLoading || disabled}
      />

      {error && <div>{error}</div>}

      {previewUrl && (
        <div>
          <h3>Preview:</h3>
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            alt="Preview"
            src={`${previewUrl}?width=150&height=150`}
            style={{
              borderRadius: "4px",
            }}
          />
        </div>
      )}
    </>
  );
};
jsx
"use client";

import { useState } from "react";

export const ImageInput = ({ uploadUrl, onImageUploaded, disabled }) => {
  const [previewUrl, setPreviewUrl] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleFileUpload = async (event) => {
    const file = event.target.files?.[0];
    if (!file) return;

    try {
      setIsLoading(true);
      setError(null);

      const res = await fetch(uploadUrl, {
        method: "POST",
        headers: {
          "X-Original-Filename": file.name,
        },
        body: file,
      });

      const data = await res.json();
      const { temporaryImageUrl } = data;

      onImageUploaded(temporaryImageUrl);
      setPreviewUrl(temporaryImageUrl);
    } catch (error) {
      console.error("Upload failed:", error);
      setError("Uploading image failed");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <>
      <input
        type="file"
        accept="image/jpeg,image/png,image/webp,image/gif,image/avif,image/tiff,image/heic"
        onChange={handleFileUpload}
        disabled={isLoading || disabled}
      />

      {error && <div>{error}</div>}

      {previewUrl && (
        <div>
          <h3>Preview:</h3>
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            alt="Preview"
            src={`${previewUrl}?width=150&height=150`}
            style={{
              borderRadius: "4px",
            }}
          />
        </div>
      )}
    </>
  );
};

Step 3: Use the ImageInput Component

Now use the ImageInput component in your page or form:

tsx
"use client";

import { useState } from "react";
import { ImageInput } from "./ImageInput";

export default function ProfilePage() {
  const [temporaryImageUrl, setTemporaryImageUrl] = useState<string>("");

  return (
    <div>
      <h1>Update Profile Picture</h1>
      <ImageInput
        uploadUrl={process.env.NEXT_PUBLIC_ICEFIERY_UPLOAD_URL!}
        onImageUploaded={setTemporaryImageUrl}
      />

      {temporaryImageUrl && <p>Image uploaded: {temporaryImageUrl}</p>}
    </div>
  );
}
jsx
"use client";

import { useState } from "react";
import { ImageInput } from "./ImageInput";

export default function ProfilePage() {
  const [temporaryImageUrl, setTemporaryImageUrl] = useState("");

  return (
    <div>
      <h1>Update Profile Picture</h1>
      <ImageInput
        uploadUrl={process.env.NEXT_PUBLIC_ICEFIERY_UPLOAD_URL}
        onImageUploaded={setTemporaryImageUrl}
      />

      {temporaryImageUrl && <p>Image uploaded: {temporaryImageUrl}</p>}
    </div>
  );
}

Step 4: Create API Route

After uploading an image, you'll receive a temporaryImageUrl. This temporary URL expires after a short time, so you need to save it permanently to your project.

Create an API route to securely save the temporary image using your private API key.

ts
// app/api/update-profile-picture/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  // Get from session/auth: getServerSession(), auth.userId, etc.
  const userId = "user_12345";

  // Read temporaryImageUrl from body
  const body: { temporaryImageUrl: string } = await request.json();
  const { temporaryImageUrl } = body;

  // Save the temporary image permanently
  const response = await fetch(
    "https://api.icefiery.com/api/v1/save-temporary-image",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ICEFIERY_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        temporaryImageUrl,
        metadata: {
          // Optional metadata can be stored for reference / debugging ease
          userId,
        },
      }),
    }
  );

  const { imageUrl }: { imageUrl: string } = await response.json();

  // Update user profile in database
  // await db.users.update(userId, {
  //   profilePictureUrl: imageUrl
  // });

  return NextResponse.json({ imageUrl });
}
js
// app/api/update-profile-picture/route.js
import { NextResponse } from "next/server";

export async function POST(request) {
  // Get from session/auth: getServerSession(), auth.userId, etc.
  const userId = "user_12345";

  // Read temporaryImageUrl from body
  const { temporaryImageUrl } = await request.json();

  // Save the temporary image permanently
  const response = await fetch(
    "https://api.icefiery.com/api/v1/save-temporary-image",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ICEFIERY_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        temporaryImageUrl,
        metadata: {
          // Optional metadata can be stored for reference / debugging ease
          userId,
        },
      }),
    }
  );

  const { imageUrl } = await response.json();

  // Update user profile in database
  // await db.users.update(userId, {
  //   profilePictureUrl: imageUrl
  // });

  return NextResponse.json({ imageUrl });
}
ts
// pages/api/update-profile-picture.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // Get from session/auth: req.session.userId, getToken(), etc.
  const userId = "user_12345";

  // Read temporaryImageUrl from body
  const body: { temporaryImageUrl: string } = req.body;
  const { temporaryImageUrl } = body;

  // Save the temporary image permanently
  const response = await fetch(
    "https://api.icefiery.com/api/v1/save-temporary-image",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ICEFIERY_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        temporaryImageUrl,
        metadata: {
          // Optional metadata can be stored for reference / debugging ease
          userId,
        },
      }),
    }
  );

  const { imageUrl }: { imageUrl: string } = await response.json();

  // Update user profile in database
  // await db.users.update(userId, {
  //   profilePictureUrl: imageUrl
  // });

  res.json({ imageUrl });
}
js
// pages/api/update-profile-picture.js
export default async function handler(req, res) {
  // Get from session/auth: req.session.userId, getToken(), etc.
  const userId = "user_12345";

  // Read temporaryImageUrl from body
  const { temporaryImageUrl } = req.body;

  // Save the temporary image permanently
  const response = await fetch(
    "https://api.icefiery.com/api/v1/save-temporary-image",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ICEFIERY_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        temporaryImageUrl,
        metadata: {
          // Optional metadata can be stored for reference / debugging ease
          userId,
        },
      }),
    }
  );

  const { imageUrl } = await response.json();

  // Update user profile in database
  // await db.users.update(userId, {
  //   profilePictureUrl: imageUrl
  // });

  res.json({ imageUrl });
}

Step 5: Save the Image with Form Submission

Now update your page to include a form that saves the temporary image permanently using your API route:

tsx
"use client";

import { useState } from "react";
import { ImageInput } from "./ImageInput";

export default function ProfilePage() {
  const [temporaryImageUrl, setTemporaryImageUrl] = useState<string>("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    if (!temporaryImageUrl) return;

    try {
      setIsSubmitting(true);

      const response = await fetch("/api/update-profile-picture", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ temporaryImageUrl }),
      });

      const { imageUrl } = await response.json();
      console.log("Saved image:", imageUrl);
      alert("Profile picture updated!");
    } catch (error) {
      console.error("Failed to save image:", error);
      alert("Failed to update profile picture");
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div>
      <h1>Update Profile Picture</h1>
      <form onSubmit={handleSubmit}>
        <ImageInput
          uploadUrl={process.env.NEXT_PUBLIC_ICEFIERY_UPLOAD_URL!}
          onImageUploaded={setTemporaryImageUrl}
          disabled={isSubmitting}
        />

        <button type="submit" disabled={!temporaryImageUrl || isSubmitting}>
          {isSubmitting ? "Saving..." : "Save Profile Picture"}
        </button>
      </form>
    </div>
  );
}
jsx
"use client";

import { useState } from "react";
import { ImageInput } from "./ImageInput";

export default function ProfilePage() {
  const [temporaryImageUrl, setTemporaryImageUrl] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();
    if (!temporaryImageUrl) return;

    try {
      setIsSubmitting(true);

      const response = await fetch("/api/update-profile-picture", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ temporaryImageUrl }),
      });

      const { imageUrl } = await response.json();
      console.log("Saved image:", imageUrl);
      alert("Profile picture updated!");
    } catch (error) {
      console.error("Failed to save image:", error);
      alert("Failed to update profile picture");
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div>
      <h1>Update Profile Picture</h1>
      <form onSubmit={handleSubmit}>
        <ImageInput
          uploadUrl={process.env.NEXT_PUBLIC_ICEFIERY_UPLOAD_URL}
          onImageUploaded={setTemporaryImageUrl}
          disabled={isSubmitting}
        />

        <button type="submit" disabled={!temporaryImageUrl || isSubmitting}>
          {isSubmitting ? "Saving..." : "Save Profile Picture"}
        </button>
      </form>
    </div>
  );
}

Step 6: Configure Next.js to allow Icefiery domain

In order to serve images from Icefiery, add it to allowed image domains in your Next.js configuration:

javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // For Next.js 12.3 and earlier
    domains: ["cdn.icefiery.com"],

    // For Next.js 13+ (recommended)
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.icefiery.com",
        pathname: "/res/**",
      },
    ],
  },
};

module.exports = nextConfig;

All Done!

You've successfully integrated Icefiery into your Next.js application! Your users can now upload images which are temporarily stored, then permanently saved to your project with proper error handling and loading states.

Next Steps