Appearance
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
- Next.js Image Loader - Optimize images with Next.js Image component
- Docker Tutorial - Run Icefiery locally for development purposes
- Managing API Keys - Set up secure access
- Organizing with Projects - Manage images effectively
- API Reference - Complete API documentation