blocked, especially if it involves modifying headers or triggering a download.
β Typical Client-Side Approach Fails:
const response = await fetch("https://external-server.com/image.jpg");
// β Blocked by CORS
So how do we safely download an image from an external server?
π‘ The Solution: Server-Side Proxy
Instead of downloading the image directly from the frontend, we implemented a server-side proxy in a Next.js API route. This approach has several benefits:
-
Bypasses CORS: Server-to-server requests are not bound by CORS rules.
-
Custom headers: You can control content-disposition and other headers.
-
Security & Abstraction: The client never interacts directly with the external image host.
π οΈ Step-by-Step Implementation
1. Create a Proxy API Route: /api/download-image/route.ts
This API endpoint acts as a middleman between the client and the image server.
β
Logic:
- Accepts two query params:
url (the image URL), and filename (optional, for saving the file).
- Fetches the image from the external source.
- Sets appropriate response headers to force a download.
- Pipes the image back to the client.
π Example: app/api/download-image/route.ts
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const url = searchParams.get("url");
const filename = searchParams.get("filename") || "downloaded-image";
if (!url) {
return new Response("Missing image URL", { status: 400 });
}
try {
const response = await fetch(url);
if (!response.ok) throw new Error("Image fetch failed");
const contentType = response.headers.get("content-type") || "application/octet-stream";
const buffer = await response.arrayBuffer();
return new Response(buffer, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
"Cache-Control": "no-cache",
},
});
} catch (error) {
return new Response("Error downloading image", { status: 500 });
}
}
2. Update the Frontend Download Function
Instead of hitting the external image URL directly, your frontend now calls your proxy endpoint.
β
Function Example:
const downloadImage = async (imageUrl: string, filename: string) => {
const proxyUrl = `/api/download-image?url=${encodeURIComponent(imageUrl)}&filename=${encodeURIComponent(filename)}`;
const link = document.createElement("a");
link.href = proxyUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
π What This Does:
- Constructs the proxy URL.
- Triggers a download via a hidden anchor element.
- The proxy handles the image fetch and sets the proper headers.
π§ͺ Example Usage
<button
onClick={() =>
downloadImage("https://example.com/image.jpg", "cool-image.jpg")
}
>
Download Image
</button>
β
Benefits Recap
| Feature |
Why It Matters |
| π« No CORS issues |
Server-to-server communication bypasses CORS |
| π― Filename control |
Set desired filename with Content-Disposition |
| π More secure |
Client doesn't interact directly with external servers |
| π§© Easily extensible |
Add auth, image validation, logging, etc. |
π Conclusion
Downloading images from third-party sources on the frontend can be tricky due to CORS restrictions, but implementing a server-side proxy in Next.js offers a clean, scalable solution. Whether you're building a design tool, a content editor, or just need to enable image exports, this method ensures a smooth and secure user experience.
βοΈ Tip: You can reuse this pattern for other file types (PDFs, audio, video) too!