یکی از محدودیتهایی که بسیاری از توسعهدهندگان هنگام کار با Next.js با آن روبهرو میشوند، مدیریت فایلهایی است که کاربران پس از Deploy برنامه آپلود میکنند.
به صورت پیشفرض، Next.js تنها فایلهای موجود در پوشه public را به عنوان فایلهای استاتیک سرویسدهی میکند. این فایلها هنگام Build شدن پروژه در خروجی نهایی قرار میگیرند و پس از استقرار برنامه دیگر امکان اضافه شدن فایل جدید به این پوشه وجود ندارد.
به همین دلیل اگر کاربران بتوانند تصویر پروفایل، فایل PDF، ویدیو یا هر فایل دیگری را آپلود کنند، قرار دادن آنها داخل public راهکار مناسبی نخواهد بود. در این مقاله یک سیستم کامل برای آپلود، ذخیره و سرویسدهی فایلها با استفاده از API Routes در Next.js پیادهسازی میکنیم.
چرا از API Route استفاده کنیم؟
استفاده از یک API سفارشی مزایای زیادی برای مدیریت فایلها ایجاد میکند:
- امکان ذخیره فایلها در هر پوشه دلخواه روی سرور
- جلوگیری از دسترسی مستقیم کاربران به فایلهای حساس
- کنترل کامل روی مجوزهای دسترسی و احراز هویت
- امکان تعیین هدرهای پیشرفته Cache برای بهبود عملکرد
- امکان اعتبارسنجی کاربران قبل از دانلود فایل
- قابلیت ثبت لاگ دقیق از عملیات دانلود و آپلود فایلها
- توسعه آسان برای اتصال به سرویسهای ابری مانند Amazon S3، Cloudinary یا MinIO
ساخت پوشه Upload
ابتدا یک پوشه برای نگهداری فایلها در ریشه پروژه ایجاد میکنیم:
uploads/
یا میتوان مسیر دلخواه را از طریق متغیرهای محیطی در فایل .env مشخص کرد:
UPLOAD_DIR=/var/www/uploads
در صورتی که این مقدار تنظیم نشده باشد، پروژه به صورت پیشفرض از دایرکتوری زیر استفاده خواهد کرد:
project-root/uploads
ساخت API برای سرویسدهی فایلها
برای ساخت مسیر داینامیک جهت خواندن فایلها، ابتدا فایل زیر را ایجاد کنید:
src/app/api/uploads/[...path]/route.ts
سپس کد زیر را برای مدیریت درخواستهای GET در آن قرار دهید:
import fs from "fs/promises";
import path from "path";
import { NextRequest } from "next/server";
const UPLOAD_DIR =
process.env.UPLOAD_DIR ||
path.join(process.cwd(), "uploads");
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const { path: parts } = await params;
const resolved = path.resolve(
UPLOAD_DIR,
...parts,
);
// جلوگیری از حمله Path Traversal
if (!resolved.startsWith(path.resolve(UPLOAD_DIR))) {
return new Response("Forbidden", {
status: 403,
});
}
const filePath = path.join(
UPLOAD_DIR,
...parts,
);
try {
const file = await fs.readFile(filePath);
return new Response(file, {
headers: {
"Content-Type": getContentType(filePath),
"Cache-Control": "public,max-age=31536000",
},
});
} catch {
return new Response("Not Found", {
status: 404,
});
}
}
function getContentType(file: string) {
switch (path.extname(file).toLowerCase()) {
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".gif":
return "image/gif";
case ".webp":
return "image/webp";
case ".svg":
return "image/svg+xml";
case ".pdf":
return "application/pdf";
case ".mp4":
return "video/mp4";
default:
return "application/octet-stream";
}
}
این API چگونه کار میکند؟
تعیین مسیر ذخیره فایلها
ابتدا مسیر اصلی فایلها مشخص میشود. اگر متغیر محیطی وجود داشته باشد همان استفاده میشود؛ در غیر این صورت پوشه uploads داخل ریشه پروژه انتخاب میشود.
const UPLOAD_DIR =
process.env.UPLOAD_DIR ||
path.join(process.cwd(), "uploads");
استفاده از Catch-all Route
نام پوشه [...path] باعث میشود تمام قسمتهای URL داخل یک آرایه قرار بگیرند. برای مثال، آدرس /api/uploads/images/avatar.png به شکل زیر در متغیر parts دریافت میشود:
["images", "avatar.png"]
جلوگیری از حمله Path Traversal
یکی از مهمترین بخشهای این کد، امنیت آن است. کاربر ممکن است درخواستهایی مثل ../../../.env ارسال کند تا به فایلهای حساس سرور دسترسی پیدا کند. به همین دلیل ابتدا مسیر نهایی محاسبه شده و سپس بررسی میشود که حتماً داخل دایرکتوری مجاز uploads باشد، در غیر این صورت پاسخ 403 Forbidden برگردانده میشود.
if (!resolved.startsWith(path.resolve(UPLOAD_DIR)))
تعیین Content-Type و کش کردن
مرورگر برای نمایش صحیح فایل باید نوع آن را بداند. تابع getContentType با توجه به پسوند فایل، هدر مناسب را ارسال میکند. همچنین هدر Cache-Control باعث میشود فایل تا یک سال در مرورگر کش شود که نتیجه آن افزایش سرعت لود و مصرف کمتر پهنای باند است.
ساخت API برای آپلود فایل
اکنون باید امکان دریافت و ذخیره فایلها را از سمت کلاینت فراهم کنیم. فایل زیر را ایجاد کنید:
src/app/api/upload/route.ts
سپس کد مربوط به متد POST را در آن قرار دهید:
import fs from "fs/promises";
import path from "path";
const UPLOAD_DIR =
process.env.UPLOAD_DIR ||
path.join(process.cwd(), "uploads");
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return Response.json(
{ message: "فایلی ارسال نشده است." },
{ status: 400 },
);
}
await fs.mkdir(UPLOAD_DIR, {
recursive: true,
});
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const fileName =
Date.now() + "-" + file.name;
const filePath = path.join(
UPLOAD_DIR,
fileName,
);
await fs.writeFile(filePath, buffer);
return Response.json({
success: true,
fileName,
url: `/api/uploads/${fileName}`,
});
}
ارسال فایل از سمت کلاینت
در سمت فرانتاند (React/Next.js) میتوانید فایل انتخاب شده را به راحتی از طریق FormData به API ارسال کنید:
const formData = new FormData();
formData.append("file", selectedFile);
await fetch("/api/upload", {
method: "POST",
body: formData,
});
در پاسخ، شیء زیر را دریافت خواهید کرد که شامل آدرس دسترسی استاتیک به فایل است:
{
"success": true,
"fileName": "1745300195-avatar.png",
"url": "/api/uploads/1745300195-avatar.png"
}
از این آدرس میتوانید مستقیماً در تگهای HTML پروژه خود استفاده کنید:
<img src="/api/uploads/1745300195-avatar.png" />
<a href="/api/uploads/document.pdf">دانلود فایل</a>
پیشنهادهایی برای افزایش امنیت
اگر قصد دارید این سیستم را در محیط واقعی و Production استفاده کنید، برای امنیت بیشتر حتماً موارد زیر را اعمال کنید:
- محدود کردن حداکثر حجم فایلهای ورودی (مثلاً تا ۵ مگابایت)
- بررسی دقیق
MIME Type فایل جهت اطمینان از ساختار واقعی آن
- جلوگیری سختگیرانه از آپلود فایلهای اجرایی و خطرناک مانند
exe، .bat. و sh.
- تغییر نام فایلها با شناسه ساختاری منحصربهفرد (مانند UUID) به جای نام اصلی کلاینت
- احراز هویت (Authentication) کاربران قبل از اجازه آپلود یا دانلود
- محدود کردن تعداد درخواستها (Rate Limiting) برای جلوگیری از حملات DoS
- ذخیره اطلاعات و آدرس فایل در دیتابیس جهت مدیریت آسانتر
ساختار پیشنهادی پروژه
معماری نهایی پوشهها و فایلهای ایجاد شده در این مقاله به صورت زیر خواهد بود:
src
└── app
└── api
├── upload
│ └── route.ts
│
└── uploads
└── [...path]
└── route.ts
uploads
├── images
├── documents
└── videos
جمعبندی
اگرچه Next.js برای فایلهای ثابت موجود در پوشه public عملکرد بسیار خوبی دارد، اما برای فایلهایی که کاربران در زمان اجرای برنامه آپلود میکنند، این روش پاسخگو نیست. استفاده از API Routes یک راهکار کاملاً منعطف، ایمن و قابل توسعه است که امکاناتی مانند کنترل سطح دسترسی، مدیریت کش سفارشی و اعتبارسنجی فرآیندها را در اختیار شما قرار میدهد.
در پروژههای کوچک و متوسط میتوان فایلها را روی دیسک سرور ذخیره کرد، اما مزیت اصلی این معماری در آن است که در صورت بزرگ شدن پروژه، میتوانید بدون تغییر در ساختار فرانتاند، بخش Backend این API را بهراحتی به فضاهای ابری پیشرفته مانند Amazon S3، Cloudinary یا MinIO متصل کنید.
یکی از محدودیتهایی که بسیاری از توسعهدهندگان هنگام کار با Next.js با آن روبهرو میشوند، مدیریت فایلهایی است که کاربران پس از Deploy برنامه آپلود میکنند.
به صورت پیشفرض، Next.js تنها فایلهای موجود در پوشه
publicرا به عنوان فایلهای استاتیک سرویسدهی میکند. این فایلها هنگام Build شدن پروژه در خروجی نهایی قرار میگیرند و پس از استقرار برنامه دیگر امکان اضافه شدن فایل جدید به این پوشه وجود ندارد.به همین دلیل اگر کاربران بتوانند تصویر پروفایل، فایل PDF، ویدیو یا هر فایل دیگری را آپلود کنند، قرار دادن آنها داخل
publicراهکار مناسبی نخواهد بود. در این مقاله یک سیستم کامل برای آپلود، ذخیره و سرویسدهی فایلها با استفاده از API Routes در Next.js پیادهسازی میکنیم.چرا از API Route استفاده کنیم؟
استفاده از یک API سفارشی مزایای زیادی برای مدیریت فایلها ایجاد میکند:
ساخت پوشه Upload
ابتدا یک پوشه برای نگهداری فایلها در ریشه پروژه ایجاد میکنیم:
یا میتوان مسیر دلخواه را از طریق متغیرهای محیطی در فایل
.envمشخص کرد:در صورتی که این مقدار تنظیم نشده باشد، پروژه به صورت پیشفرض از دایرکتوری زیر استفاده خواهد کرد:
ساخت API برای سرویسدهی فایلها
برای ساخت مسیر داینامیک جهت خواندن فایلها، ابتدا فایل زیر را ایجاد کنید:
سپس کد زیر را برای مدیریت درخواستهای GET در آن قرار دهید:
این API چگونه کار میکند؟
تعیین مسیر ذخیره فایلها
ابتدا مسیر اصلی فایلها مشخص میشود. اگر متغیر محیطی وجود داشته باشد همان استفاده میشود؛ در غیر این صورت پوشه
uploadsداخل ریشه پروژه انتخاب میشود.استفاده از Catch-all Route
نام پوشه
[...path]باعث میشود تمام قسمتهای URL داخل یک آرایه قرار بگیرند. برای مثال، آدرس/api/uploads/images/avatar.pngبه شکل زیر در متغیرpartsدریافت میشود:جلوگیری از حمله Path Traversal
یکی از مهمترین بخشهای این کد، امنیت آن است. کاربر ممکن است درخواستهایی مثل
../../../.envارسال کند تا به فایلهای حساس سرور دسترسی پیدا کند. به همین دلیل ابتدا مسیر نهایی محاسبه شده و سپس بررسی میشود که حتماً داخل دایرکتوری مجازuploadsباشد، در غیر این صورت پاسخ403 Forbiddenبرگردانده میشود.تعیین Content-Type و کش کردن
مرورگر برای نمایش صحیح فایل باید نوع آن را بداند. تابع
getContentTypeبا توجه به پسوند فایل، هدر مناسب را ارسال میکند. همچنین هدرCache-Controlباعث میشود فایل تا یک سال در مرورگر کش شود که نتیجه آن افزایش سرعت لود و مصرف کمتر پهنای باند است.ساخت API برای آپلود فایل
اکنون باید امکان دریافت و ذخیره فایلها را از سمت کلاینت فراهم کنیم. فایل زیر را ایجاد کنید:
سپس کد مربوط به متد POST را در آن قرار دهید:
ارسال فایل از سمت کلاینت
در سمت فرانتاند (React/Next.js) میتوانید فایل انتخاب شده را به راحتی از طریق
FormDataبه API ارسال کنید:در پاسخ، شیء زیر را دریافت خواهید کرد که شامل آدرس دسترسی استاتیک به فایل است:
از این آدرس میتوانید مستقیماً در تگهای HTML پروژه خود استفاده کنید:
پیشنهادهایی برای افزایش امنیت
اگر قصد دارید این سیستم را در محیط واقعی و Production استفاده کنید، برای امنیت بیشتر حتماً موارد زیر را اعمال کنید:
MIME Typeفایل جهت اطمینان از ساختار واقعی آنexe،.bat.وsh.ساختار پیشنهادی پروژه
معماری نهایی پوشهها و فایلهای ایجاد شده در این مقاله به صورت زیر خواهد بود:
جمعبندی
اگرچه Next.js برای فایلهای ثابت موجود در پوشه
publicعملکرد بسیار خوبی دارد، اما برای فایلهایی که کاربران در زمان اجرای برنامه آپلود میکنند، این روش پاسخگو نیست. استفاده از API Routes یک راهکار کاملاً منعطف، ایمن و قابل توسعه است که امکاناتی مانند کنترل سطح دسترسی، مدیریت کش سفارشی و اعتبارسنجی فرآیندها را در اختیار شما قرار میدهد.در پروژههای کوچک و متوسط میتوان فایلها را روی دیسک سرور ذخیره کرد، اما مزیت اصلی این معماری در آن است که در صورت بزرگ شدن پروژه، میتوانید بدون تغییر در ساختار فرانتاند، بخش Backend این API را بهراحتی به فضاهای ابری پیشرفته مانند Amazon S3، Cloudinary یا MinIO متصل کنید.