When uploading images to Cloudinary programmatically, you can do it from the frontend or the backend, depending on the app’s needs. Frontend uploads are more common in customer-facing apps, where speed is critical, and you want the image to be on a server somewhere as soon as possible without any proxies in between. Backend uploads, on the other hand, are used when you want the image to be passed through your API or in asynchronous operations and scheduled jobs.
Imagine an AI-based image transformation app where users upload images they want to be transformed using AI. An AI transformation may take quite a bit of time already, so you want to optimize the speed wherever possible. By uploading to Cloudinary from the frontend client, you’ll upload your image to a geographically close region, which is then replicated to the vast number of Cloudinary CDN servers automatically.
Sometimes, there are other factors involved, too. Perhaps you’re limited by the payload size of your serverless platform and can’t send large images. Or you just want to save on bandwidth. In such cases, you’ll want the images to be uploaded to Cloudinary by the frontend, so you can just send a URL to your backend API.
In all cases, images should be uploaded to Cloudinary securely using your Cloudinary API secret. When uploading from the backend, that’s an easier task. You only need to use the Cloudinary SDK, initialize it with the API key and secret, and then use the upload API to upload the image. On the frontend, things aren’t so simple. Everything shipped to the browser is theoretically readable by the user, so you don’t want to expose your API secret to the user. Otherwise, a malicious user might use it to upload any images to your Cloudinary account, spending your plan’s tokens, which is not something that you intended.
There’s a workaround combining the security of the backend with the upload flexibility of the frontend, generating signed URLs for upload. In this blog post, we’ll show you how this can be done.
The easiest way to generate signed upload URLs is to use the Cloudinary SDK. We’ll show you an example using Next.js and the Node SDK.
For simplicity, we’ll use Next.js server functions to perform code meant to be executed by the backend. If you use some alternative backend implementations, you can still use this as a guideline. For those of you not familiar with Next.js server functions, they are almost like RPCs (Remote Procedure Calls) exposed to the client running in the browser. When a server function is called, the client sends a POST request to the Next.js backend in the background.
As with any publicly accessible API, protecting it is always a good idea. The first step is to check if an authorized user has made the request. The next step is to initialize the Cloudinary SDK by providing it with your cloud name, API key, and API secret. You can find your Cloudinary cloud name by logging into your Cloudinary account and navigating to the settings page of your dashboard in API Keys. The cloud name will be noted at the top of the page. This is also the place to obtain your API key and secret. All of this information should be stored securely in environment variables on the server.
After initializing the SDK, the next step is to create a cryptographic signature that will be used for image uploads. Without the signature, anonymous users cannot upload to your Cloudinary account. The signature takes some parameters, such as timestamp, upload folder, and tags (if you would like to tag your images). These parameters, in addition to the signature, are then provided to the frontend. The only piece not provided to the frontend is your Cloudinary secret; as its name says, this should be kept secret and not exposed outside the back end.
The complete code is below. The module is annotated with the use server
directive to mark it as a module that contains server functions exposed to the client.
"use server";
import { v2 as cloudinary } from "cloudinary";
import { getCurrentUser } from '@/lib/auth.ts';
const CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME || "";
const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY || "";
const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET || "";
const CLOUDINARY_UPLOAD_FOLDER = process.env.CLODINARY_UPLOAD_FOLDER || "";
export const generateCloudinarySignatureAction = async () => {
// The implementation of getCurrentUser is not relevant for this blog post
// However, this is only to show you that you need to protect your server actions
// if image upload is only for authenticated users
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error("User not authenticated")
}
// Configure your Cloudinary instance with the properties obtained from the environment
cloudinary.config({
cloud_name: CLOUDINARY_CLOUD_NAME,
api_key: CLOUDINARY_API_KEY,
api_secret: CLOUDINARY_API_SECRET,
});
// Every signature is parametrized for the specific upload needed
const paramsToSign = {
timestamp: Math.floor(new Date().getTime() / 1000), // Unix timestamp in seconds
folder: CLOUDINARY_UPLOAD_FOLDER, // The folder to upload the image to
tags: "avatar-image", // Optionally, tags to add to the image, comma separated
};
// Call the Cloudinary SDK to sign the parameters
const signature = cloudinary.utils.api_sign_request(
paramsToSign,
CLOUDINARY_API_SECRET,
);
// All of the following properties are needed on the frontend to perform the upload
return {
signature: signature,
apiKey: CLOUDINARY_API_KEY,
cloudName: CLOUDINARY_CLOUD_NAME,
timestamp: paramsToSign.timestamp,
folder: paramsToSign.folder,
};
};
Code language: JavaScript (javascript)
On the client, we’ll show how this works by working on top of an imaginary Avatar
component, which provides an onClickUpload
handler called when the user wants to change their avatar. In the handler, we first invoke the server function. In the background, Next.js makes a POST request to the backend and returns the signature result object.
We’ll then create a FormData object and attach the properties from the signature and the image file. We can then upload the image to Cloudinary using a plain fetch
call. The response will be an object with the secure_url
property, which contains the URL of the uploaded image. We can then send this URL to the backend or use it to update the client’s avatar immediately.
The code is below:
"use client"
import { useState } from "react"
import { Home, Settings, User } from "lucide-react"
import { Avatar } from "@/components/avatar"
import { Button } from "@/components/ui/button"
import { generateCloudinarySignatureAction, updateUserAvatar } from "@/lib/actions"
export function Sidebar() {
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
const [isUploading, setIsUploading] = useState(false)
const handleAvatarUpload = async (imageFile: File) => {
try {
setIsUploading(true)
// Generate a signed upload URL from Cloudinary
const signatureResult = await generateCloudinarySignatureAction()
// Create form data for the upload
const formData = new FormData()
formData.append("file", imageFile)
// Alternatively, you can also read this from something like process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY
formData.append("api_key", signatureResult.apiKey);
formData.append("timestamp", `${signatureResult.timestamp}`);
formData.append("signature", signatureResult.signature);
formData.append("folder", signatureResult.folder);
formData.append("tags", "avatar-image");
// Upload to Cloudinary
const uploadResponse = await fetch(uploadUrl, {
method: "POST",
body: formData,
})
const uploadResult = await uploadResponse.json()
if (uploadResult.secure_url) {
// Update the user's avatar in the database
await updateUserAvatarAction(uploadResult.secure_url)
setAvatarUrl(uploadResult.secure_url)
}
} catch (error) {
console.error("Error uploading avatar", error)
} finally {
setIsUploading(false)
}
}
return (
<div>
{ /** Other components **/ }
<Avatar url={avatarUrl} onClickUpload={handleAvatarUpload} isUploading={isUploading} />
{ /** Other components **/ }
</div>
)
}
Code language: JavaScript (javascript)
It’s also important to note at this point that the above code shows how to upload the image manually, in case you would want to have complete control over the entire process. If you’d just like to get running as soon as possible, you can also use Cloudinary’s upload widget (although the signature generation part on the backend is the same).
Once a signature is generated with certain parameters, it’s only valid for those parameters. This is an important point to consider when making changes to the backend API. Imagine, for example, that you have a long-running frontend app that uses backend-generated signatures to upload images to a user gallery. Then, at some point, you want to introduce tagging for such images by tagging each uploaded image with gallery
. The backend is deployed, and you start seeing failed user uploads in your logs. This means some users obtained the signature from the old API and then attempted to upload. Since signatures are also used to validate that no parameters originally meant for the image were tampered with (a good security measure!), those older upload URLs are no longer valid.
To avoid this, always make sure to do proper change management of your API, via API versioning, for example.
Cloudinary’s signed upload URLs are another tool you can use with Cloudinary to upload your assets. With them, image uploads aren’t only restricted to backend services but can be used directly by the end-user, which securely provides speed and simplicity.
Sign up for a free Cloudinary account to try it out for yourself.