قسمت شانزدهم - Route Handlers در Next.js
درNext.js چند روش برای اتصال فرمها و UI به منطق سمت سرور وجود دارد. دو روش مهمی که در پروژههای App Router زیاد با آنها روبهرو میشویم عبارتاند از: - Server Actions - Route Handlers
اتصال فرمها به بکاند در Next.js: Server Actions یا Route Handlers؟
در قسمتهای قبل، اول با ساختار فرمها در Next.js آشنا شدیم و بعد یاد گرفتیم چطور دادههای فرم را با Zod اعتبارسنجی کنیم.
تا اینجا هنوز فرمها را به بکاند وصل نکردهایم. یعنی فرم داریم، input داریم، schema اعتبارسنجی داریم، اما هنوز مشخص نکردهایم وقتی کاربر روی دکمه ارسال کلیک میکند، داده باید به کجا برود و چطور پردازش شود.
در Next.js چند روش برای اتصال فرمها و UI به منطق سمت سرور وجود دارد. دو روش مهمی که در پروژههای App Router زیاد با آنها روبهرو میشویم عبارتاند از:
Server ActionsRoute Handlers
هر دو میتوانند کد سمت سرور اجرا کنند. هر دو میتوانند داده دریافت کنند. هر دو میتوانند به دیتابیس، سرویس ایمیل یا سرویسهای خارجی وصل شوند. اما هدف، مدل ذهنی و محل استفاده آنها یکسان نیست.
در این قسمت میخواهیم تفاوت این دو روش را بفهمیم و بعد مسیر اصلی پروژه را مشخص کنیم: برای ساخت CRUD قابل توسعه، مخصوصاً در بخشهایی مثل پروژهها، مقالات، داشبورد و CMS، تمرکز ما روی Route Handlers خواهد بود.
مسئله اصلی چیست؟
فرض کن یک فرم ایجاد پروژه داریم:
<form>
<input name="title" />
<input name="slug" />
<textarea name="description" />
<button type="submit">Create Project</button>
</form>
وقتی کاربر فرم را ارسال میکند، چند اتفاق باید بیفتد:
- داده از فرم خوانده شود
- داده اعتبارسنجی شود
- اگر نامعتبر بود، خطا برگردد
- اگر معتبر بود، در دیتابیس ذخیره شود
- پاسخ مناسب به کاربر داده شود
سؤال اینجاست:
این منطق را کجا بنویسیم؟
در Next.js میتوانیم این کار را با Server Actions انجام دهیم. همچنین میتوانیم یک endpoint با Route Handler بسازیم و فرم یا client را به آن endpoint وصل کنیم.
برای اینکه تصمیم درستی بگیریم، اول باید هرکدام را درست بشناسیم.
Server Actions چیست؟
Server Actions قابلیتی در Next.js است که اجازه میدهد یک تابع سمت سرور را مستقیماً از فرم یا کامپوننت فراخوانی کنیم.
مثلاً میتوانیم یک تابع سمت سرور داشته باشیم:
'use server'
export async function createProject(formData: FormData) {
const title = String(formData.get('title') ?? '')
const slug = String(formData.get('slug') ?? '')
// validate data
// save to database
}
و بعد آن را به فرم بدهیم:
import { createProject } from './actions'
export function ProjectForm() {
return (
<form action={createProject}>
<input name="title" />
<input name="slug" />
<button type="submit">Create</button>
</form>
)
}
در این مدل، فرم مستقیماً به یک تابع سمت سرور وصل میشود. نیازی نیست خودمان endpoint جداگانه بسازیم و نیازی نیست با fetch درخواست ارسال کنیم.
این روش برای بعضی سناریوها بسیار ساده و تمیز است.
Server Actions چه زمانی مناسب است؟
Server Actions بیشتر برای زمانی مناسب است که عملیات مستقیماً از همان UI انجام میشود و نیاز نداریم یک API عمومی یا قابل استفاده مجدد بسازیم.
مثلاً:
- ارسال یک فرم ساده
- تغییر وضعیت یک آیتم از داخل داشبورد
- حذف یک رکورد از همان صفحه
- ثبت یک نظر ساده
- بهروزرسانی یک مقدار کوچک در دیتابیس
مثلاً یک دکمه برای انتشار یا عدم انتشار مقاله:
<form action={togglePostStatus}>
<input type="hidden" name="postId" value={post.id} />
<button type="submit">Toggle Publish</button>
</form>
در چنین مواردی، Server Actions میتواند کد را کوتاهتر کند و تجربه توسعه سادهتری بدهد.
اما این روش همیشه بهترین انتخاب نیست.
محدودیت Server Actions
طبق مستندات رسمی Next.js، هدف اصلی Server Actions اجرای کد سمت سرور از سمت client برای تغییر داده است. یعنی بیشتر برای mutationها طراحی شدهاند، نه برای ساخت یک لایه API کامل.
همچنین Server Actions برای data fetching انتخاب اصلی نیستند، چون اجرای آنها میتواند حالت صفی و ترتیبی داشته باشد و برای دریافت داده، مدل مناسبی مثل یک API endpoint یا خواندن مستقیم داده در Server Component معمولاً بهتر است.
برای پروژهای مثل پرتفولیو یا CMS شخصی، ما فقط یک فرم ساده نداریم. قرار است به این بخشها برسیم:
- ساخت پروژه
- دریافت لیست پروژهها
- دریافت یک پروژه با
idیاslug - ویرایش پروژه
- حذف پروژه
- ساخت مقاله
- ویرایش مقاله
- آپلود تصویر
- اتصال به داشبورد
- ساخت endpointهای قابل تست و قابل توسعه
در چنین مسیری، بهتر است از ابتدا مدل HTTP API را درست یاد بگیریم. اینجاست که Route Handlers وارد میشوند.
Route Handlers چیست؟
Route Handlers روش اصلی ساخت endpoint در App Router است.
در Next.js، میتوانیم داخل پوشه app فایلی به نام route.ts بسازیم و در آن برای متدهای مختلف HTTP مثل GET، POST، PATCH و DELETE handler تعریف کنیم.
مثلاً:
// app/api/projects/route.ts
export async function GET() {
return Response.json({
message: 'Get projects',
})
}
export async function POST(request: Request) {
return Response.json({
message: 'Create project',
})
}
این فایل باعث میشود مسیر زیر ساخته شود:
/api/projects
حالا اگر درخواست GET به این مسیر ارسال شود، تابع GET اجرا میشود.
اگر درخواست POST ارسال شود، تابع POST اجرا میشود.
این دقیقاً همان مدل رایج API در وب است.
محل قرارگیری Route Handler
طبق convention رسمی Next.js، یک Route Handler با فایل route.ts یا route.js ساخته میشود.
مثلاً:
app/api/projects/route.ts
یا برای یک مسیر داینامیک:
app/api/projects/[id]/route.ts
در مسیر اول معمولاً عملیات مربوط به مجموعه پروژهها را مینویسیم:
GET /api/projects
POST /api/projects
در مسیر دوم معمولاً عملیات مربوط به یک پروژه خاص را مینویسیم:
GET /api/projects/:id
PATCH /api/projects/:id
DELETE /api/projects/:id
پس ساختار کلی ما میتواند اینطور باشد:
app/
api/
projects/
route.ts
[id]/
route.ts
این ساختار برای آموزش CRUD بسیار واضح و استاندارد است.
متدهای پشتیبانیشده در Route Handlers
Route Handlers از متدهای رایج HTTP پشتیبانی میکنند:
GETPOSTPUTPATCHDELETEHEADOPTIONS
اگر یک متد را تعریف نکرده باشیم و کاربر با همان متد به endpoint درخواست بزند، Next.js پاسخ 405 Method Not Allowed برمیگرداند.
برای مسیر آموزشی ما، فعلاً بیشتر با این متدها کار داریم:
GET دریافت داده
POST ایجاد داده جدید
PATCH ویرایش بخشی از داده
DELETE حذف داده
این دقیقاً با عملیات CRUD هماهنگ است:
Create -> POST
Read -> GET
Update -> PATCH / PUT
Delete -> DELETE
یک مثال ساده از Route Handler
فرض کن میخواهیم یک endpoint ساده برای فرم تماس بسازیم.
مسیر فایل:
app/api/contact/route.ts
کد اولیه:
export async function POST(request: Request) {
const formData = await request.formData()
const name = String(formData.get('name') ?? '')
const email = String(formData.get('email') ?? '')
const message = String(formData.get('message') ?? '')
return Response.json({
success: true,
data: {
name,
email,
message,
},
})
}
در این مثال، با request.formData() داده فرم را میخوانیم.
اگر داده بهصورت JSON ارسال شده باشد، بهجای آن از request.json() استفاده میکنیم:
export async function POST(request: Request) {
const body = await request.json()
return Response.json({
success: true,
data: body,
})
}
پس در Route Handlers برای خواندن body درخواست، معمولاً از این متدها استفاده میکنیم:
request.json()request.formData()request.text()
نکته مهم این است که body درخواست را فقط یکبار میتوان خواند. اگر واقعاً لازم باشد دوبار خوانده شود، باید از request.clone() استفاده کنیم، اما در اکثر فرمهای معمولی به این کار نیاز نداریم.
اتصال فرم به Route Handler
حالا میتوانیم فرم را از سمت client به این endpoint وصل کنیم.
برای این کار معمولاً از fetch استفاده میکنیم.
مثلاً یک فرم ساده تماس:
'use client'
import { FormEvent, useState } from 'react'
export function ContactForm() {
const [message, setMessage] = useState('')
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
})
const result = await response.json()
if (!response.ok) {
setMessage(result.error ?? 'Something went wrong')
return
}
setMessage('Message sent successfully')
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<button type="submit">Send</button>
{message && <p>{message}</p>}
</form>
)
}
در این مدل، فرم بهجای اینکه مستقیماً به یک Server Action وصل شود، به یک endpoint درخواست میفرستد.
این endpoint میتواند از هرجایی استفاده شود:
- فرم داخل سایت
- داشبورد
- اپلیکیشن دیگر
- تست API
- ابزارهایی مثل
curlیاPostman - کتابخانههایی مثل
SWRیاReact Query
همین قابل استفاده بودن، یکی از مزیتهای اصلی Route Handlers است.
اضافه کردن Zod به Route Handler
در قسمت قبل یاد گرفتیم که Zod را نباید به یک روش خاص مثل Server Actions گره بزنیم. حالا میبینیم همان schemaها را میتوانیم داخل Route Handler استفاده کنیم.
فرض کن این schema را داریم:
// lib/validations/contact.ts
import { z } from 'zod'
export const contactFormSchema = z.object({
name: z
.string()
.trim()
.min(2, 'نام باید حداقل 2 کاراکتر باشد.'),
email: z
.email('ایمیل معتبر نیست.')
.trim()
.toLowerCase(),
message: z
.string()
.trim()
.min(10, 'پیام باید حداقل 10 کاراکتر باشد.'),
})
حالا در Route Handler:
// app/api/contact/route.ts
import { contactFormSchema } from '@/lib/validations/contact'
export async function POST(request: Request) {
const formData = await request.formData()
const input = {
name: String(formData.get('name') ?? ''),
email: String(formData.get('email') ?? ''),
message: String(formData.get('message') ?? ''),
}
const result = contactFormSchema.safeParse(input)
if (!result.success) {
return Response.json(
{
success: false,
errors: result.error.flatten().fieldErrors,
},
{ status: 400 }
)
}
return Response.json({
success: true,
data: result.data,
})
}
اینجا چند اتفاق مهم افتاده:
- داده خام از فرم خوانده شده
- داده به یک آبجکت قابل اعتبارسنجی تبدیل شده
Zodداده را بررسی کرده- در صورت خطا، پاسخ
400برگشته - در صورت موفقیت، داده معتبر در
result.dataدر دسترس است
این همان الگویی است که بعداً برای پروژهها و مقالهها هم استفاده میکنیم.
استفاده از NextRequest و NextResponse
در مثالهای ساده میتوانیم از Request و Response استاندارد وب استفاده کنیم.
اما Next.js نسخههای توسعهیافتهای هم دارد:
NextRequestNextResponse
اینها از پکیج next/server میآیند و امکانات راحتتری برای کار با URL، searchParams، cookies، redirect و پاسخ JSON میدهند.
مثلاً:
import { type NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const search = request.nextUrl.searchParams.get('search')
return NextResponse.json({
search,
})
}
در این مثال، با request.nextUrl.searchParams به query string دسترسی داریم.
برای مسیر زیر:
/api/projects?search=nextjs
مقدار search برابر میشود با:
nextjs
در ادامه مسیر، برای APIها معمولاً از NextResponse.json() استفاده میکنیم، چون خواناتر است و با امکانات Next.js هماهنگتر است.
Route Handler برای دریافت لیست دادهها
یکی از استفادههای اصلی Route Handlers، ساخت endpoint برای دریافت داده است.
مثلاً:
// app/api/projects/route.ts
import { NextResponse } from 'next/server'
const projects = [
{
id: '1',
title: 'Portfolio Website',
slug: 'portfolio-website',
},
{
id: '2',
title: 'Blog Platform',
slug: 'blog-platform',
},
]
export async function GET() {
return NextResponse.json({
success: true,
data: projects,
})
}
حالا اگر به مسیر زیر درخواست GET بزنیم:
/api/projects
پاسخ JSON میگیریم:
{
"success": true,
"data": [
{
"id": "1",
"title": "Portfolio Website",
"slug": "portfolio-website"
},
{
"id": "2",
"title": "Blog Platform",
"slug": "blog-platform"
}
]
}
در قسمت بعد، همین الگو را به Prisma وصل میکنیم تا دادهها از دیتابیس خوانده شوند.
Route Handler برای ساخت داده جدید
برای ایجاد داده جدید از POST استفاده میکنیم.
// app/api/projects/route.ts
import { NextResponse } from 'next/server'
import { projectSchema } from '@/lib/validations/project'
export async function POST(request: Request) {
const body = await request.json()
const result = projectSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{
success: false,
errors: result.error.flatten().fieldErrors,
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: true,
data: result.data,
},
{ status: 201 }
)
}
اینجا هنوز به دیتابیس وصل نشدهایم. فعلاً فقط میخواهیم مسیر کلی را ببینیم:
- درخواست
POSTدریافت میشود - body با
request.json()خوانده میشود - داده با
Zodاعتبارسنجی میشود - اگر معتبر بود، پاسخ
201 Createdبرمیگردد
در قسمت بعد، بهجای اینکه فقط result.data را برگردانیم، آن را با Prisma در دیتابیس ذخیره میکنیم.
Route Handler برای مسیرهای داینامیک
برای کار با یک آیتم خاص، معمولاً به مسیر داینامیک نیاز داریم.
مثلاً برای یک پروژه خاص:
app/api/projects/[id]/route.ts
در این مسیر میتوانیم id را از params بگیریم.
در Next.js جدید، برای تایپ بهتر میتوانیم از RouteContext استفاده کنیم:
// app/api/projects/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
_request: Request,
context: RouteContext<'/api/projects/[id]'>
) {
const { id } = await context.params
return NextResponse.json({
success: true,
id,
})
}
برای مسیر زیر:
/api/projects/123
خروجی میتواند این باشد:
{
"success": true,
"id": "123"
}
همین الگو را بعداً برای GET، PATCH و DELETE یک پروژه استفاده میکنیم.
مقایسه Server Actions و Route Handlers
حالا که هر دو روش را دیدیم، بهتر است تفاوت آنها را واضحتر کنیم.
Server Actions بیشتر برای زمانی مناسب است که:
- عملیات مستقیماً از یک فرم یا کامپوننت انجام میشود
- endpoint عمومی لازم نداریم
- منطق فقط مخصوص همان بخش از
UIاست - میخواهیم کد فرم سادهتر بماند
- عملیات از نوع تغییر داده است
اما Route Handlers زمانی مناسبتر است که:
- میخواهیم API واضح و قابل تست داشته باشیم
- چند بخش مختلف قرار است از یک endpoint استفاده کنند
- میخواهیم
GET,POST,PATCH,DELETEرا جدا و استاندارد مدیریت کنیم - قرار است
CRUDکامل بسازیم - میخواهیم فرم، داشبورد یا حتی سرویسهای خارجی به endpoint وصل شوند
- به ساختار شبیه
REST APIنیاز داریم
برای مسیر آموزشی ما، چون هدف ساخت CRUD، داشبورد و CMS است، Route Handlers انتخاب اصلی خواهد بود.
آیا Route Handlers جایگزین کامل بکاند هستند؟
طبق مستندات رسمی Next.js، میتوان از Next.js بهعنوان یک الگوی Backend for Frontend استفاده کرد.
یعنی میتوانیم داخل همان پروژه Next.js، endpointهایی بسازیم که:
- درخواستهای
HTTPرا دریافت کنند - داده را اعتبارسنجی کنند
- با دیتابیس کار کنند
- به سرویسهای خارجی وصل شوند
- پاسخ
JSON، متن، فایل، تصویر،XMLیا انواع دیگر محتوا برگردانند
اما باید یک نکته را دقیق بفهمیم:
Route Handlers یک بکاند کامل مستقل مثل یک فریمورک جداگانه نیستند، اما برای بسیاری از پروژههای فرانتاند محور، پرتفولیو، داشبورد، پنل مدیریت و CMS سبک، نقش یک لایه API بسیار کاربردی را بازی میکنند.
به این الگو معمولاً میگویند:
Backend for Frontend
یعنی بکاندی که مخصوص نیازهای همین فرانتاند طراحی شده است.
نکته مهم درباره Server Components
یک نکته مهم از مستندات رسمی Next.js این است:
اگر در Server Component به داده نیاز داریم، بهتر است داده را مستقیم از منبع اصلی بگیریم، نه اینکه از داخل Server Component به Route Handler خودمان fetch بزنیم.
یعنی اگر یک صفحه سروری داریم و میخواهیم پروژهها را از دیتابیس بخوانیم، بهتر است این کار را مستقیم انجام دهیم:
const projects = await prisma.project.findMany()
نه اینکه این کار را بکنیم:
const res = await fetch('http://localhost:3000/api/projects')
چرا؟
چون در حالت دوم یک رفتوبرگشت HTTP اضافه ایجاد میشود. حتی در زمان build هم ممکن است مشکل ایجاد شود، چون هنگام build الزاماً سروری برای پاسخ دادن به این درخواست داخلی وجود ندارد.
پس یک قانون ساده:
- در
Server Components: داده را مستقیم از دیتابیس یا منبع اصلی بخوان - در
Client Components: برای ارتباط با سرور ازRoute Handlersاستفاده کن - برای فرمها و
mutationهای ساده:Server Actionsهم میتواند گزینه خوبی باشد - برای
CRUD API: ازRoute Handlersاستفاده کن
کش شدن Route Handlers
در Next.js، Route Handlers بهصورت پیشفرض cache نمیشوند.
اما برای متد GET میتوانیم در صورت نیاز caching را فعال کنیم. مثلاً:
export const dynamic = 'force-static'
export async function GET() {
return Response.json({
projectName: 'Portfolio',
})
}
اما برای مسیر آموزشی ما، مخصوصاً وقتی با دیتابیس، داشبورد و دادههای قابل تغییر کار میکنیم، معمولاً باید حواسمان باشد که دادهها dynamic هستند.
نکته مهم:
- متدهای غیر از
GETمثلPOST,PATCH,DELETEcache نمیشوند - برای دادههای حساس یا قابل تغییر، نباید بیدقت caching فعال کنیم
- اگر endpoint به
request,headers,cookies, دیتابیس یا داده runtime وابسته باشد، رفتار آن dynamic خواهد بود
در پروژههای CRUD، معمولاً caching را با دقت و در مراحل بعدی بررسی میکنیم.
امنیت در Route Handlers
چون Route Handlers endpoint عمومی هستند، هر کسی میتواند به مسیر آنها درخواست بزند، مگر اینکه خودمان محدودشان کنیم.
پس نباید فکر کنیم چون endpoint داخل پروژه ماست، امن است.
برای هر endpoint مهم باید چند اصل را رعایت کنیم:
- داده ورودی را همیشه اعتبارسنجی کنیم
- خطاهای حساس را به کاربر نشان ندهیم
- برای مسیرهای داشبورد احراز هویت داشته باشیم
- برای عملیات مهم authorization بررسی کنیم
- داده اضافی یا حساس را در پاسخ برنگردانیم
- برای endpointهای حساس rate limit در نظر بگیریم
- فایلها و ورودیهای کاربر را قبل از استفاده بررسی کنیم
مثلاً نباید در catch، جزئیات کامل خطای دیتابیس را به کاربر برگردانیم.
بد:
return Response.json(
{
error: String(error),
},
{ status: 500 }
)
بهتر:
return Response.json(
{
error: 'Internal server error',
},
{ status: 500 }
)
جزئیات خطا میتواند در لاگ سرور بررسی شود، اما نباید مستقیم به client ارسال شود.
استفاده از try/catch در Route Handlers
برای عملیاتی که ممکن است خطا بدهند، بهتر است از try/catch استفاده کنیم.
مثلاً:
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
try {
const body = await request.json()
return NextResponse.json(
{
success: true,
data: body,
},
{ status: 201 }
)
} catch {
return NextResponse.json(
{
success: false,
error: 'Unexpected error',
},
{ status: 500 }
)
}
}
در قسمتهای بعدی وقتی Prisma اضافه شود، try/catch مهمتر هم میشود، چون عملیات دیتابیس ممکن است به دلایل مختلف شکست بخورد.
status codeهای رایج در API
وقتی Route Handler میسازیم، بهتر است پاسخها status code مناسب داشته باشند.
چند مورد رایج:
200 OK
برای درخواست موفق، مثل دریافت لیست دادهها.
201 Created
برای زمانی که داده جدید ساخته شده است.
400 Bad Request
برای وقتی داده ورودی نامعتبر است.
401 Unauthorized
برای وقتی کاربر احراز هویت نشده است.
403 Forbidden
برای وقتی کاربر احراز هویت شده، اما اجازه انجام عملیات را ندارد.
404 Not Found
برای وقتی داده موردنظر پیدا نشده است.
500 Internal Server Error
برای خطای غیرمنتظره سمت سرور.
مثلاً در اعتبارسنجی با Zod، اگر داده نامعتبر باشد، معمولاً 400 برمیگردانیم:
return NextResponse.json(
{
success: false,
errors: result.error.flatten().fieldErrors,
},
{ status: 400 }
)
ساختار پیشنهادی پاسخ API
برای اینکه کار با API سادهتر شود، بهتر است پاسخها ساختار نسبتاً ثابتی داشته باشند.
مثلاً برای موفقیت:
{
"success": true,
"data": {}
}
برای خطای اعتبارسنجی:
{
"success": false,
"errors": {
"title": ["عنوان الزامی است."]
}
}
برای خطای عمومی:
{
"success": false,
"error": "Internal server error"
}
این ساختار کمک میکند در UI راحتتر تصمیم بگیریم چه چیزی نمایش دهیم.
نمونه کاملتر: فرم تماس با Route Handler و Zod
حالا یک نمونه کاملتر از فرم تماس را ببینیم.
فایل اعتبارسنجی:
// lib/validations/contact.ts
import { z } from 'zod'
export const contactFormSchema = z.object({
name: z
.string()
.trim()
.min(2, 'نام باید حداقل 2 کاراکتر باشد.'),
email: z
.email('ایمیل معتبر نیست.')
.trim()
.toLowerCase(),
message: z
.string()
.trim()
.min(10, 'پیام باید حداقل 10 کاراکتر باشد.'),
})
فایل API:
// app/api/contact/route.ts
import { NextResponse } from 'next/server'
import { contactFormSchema } from '@/lib/validations/contact'
export async function POST(request: Request) {
try {
const formData = await request.formData()
const input = {
name: String(formData.get('name') ?? ''),
email: String(formData.get('email') ?? ''),
message: String(formData.get('message') ?? ''),
}
const result = contactFormSchema.safeParse(input)
if (!result.success) {
return NextResponse.json(
{
success: false,
errors: result.error.flatten().fieldErrors,
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: true,
message: 'Message received successfully',
},
{ status: 201 }
)
} catch {
return NextResponse.json(
{
success: false,
error: 'Internal server error',
},
{ status: 500 }
)
}
}
فرم سمت client:
'use client'
import { FormEvent, useState } from 'react'
export function ContactForm() {
const [status, setStatus] = useState('')
const [pending, setPending] = useState(false)
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setPending(true)
setStatus('')
const formData = new FormData(event.currentTarget)
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
})
const result = await response.json()
setPending(false)
if (!response.ok) {
setStatus(result.error ?? 'Please check form fields')
return
}
setStatus(result.message)
event.currentTarget.reset()
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<button type="submit" disabled={pending}>
{pending ? 'Sending...' : 'Send'}
</button>
{status && <p>{status}</p>}
</form>
)
}
این مثال هنوز ساده است، اما مسیر اصلی را نشان میدهد:
Form -> fetch -> Route Handler -> Zod -> Response
در قسمت بعد، همین مسیر را کاملتر میکنیم:
Form -> fetch -> Route Handler -> Zod -> Prisma -> Database
Route Handlers و فایلهای غیر JSON
یکی از مزیتهای Route Handlers این است که فقط برای JSON API نیستند. آنها میتوانند انواع مختلف پاسخ تولید کنند:
JSON- متن ساده
XML- فایل
- تصویر
RSSrobots.txtsitemapwebhook
مثلاً میتوانیم یک RSS feed یا یک endpoint برای webhook داشته باشیم. در پروژههای پیشرفتهتر، همین قابلیتها میتوانند برای بلاگ یا CMS مفید باشند.
اما در این سری آموزشی، تمرکز اصلی ما فعلاً روی JSON API و CRUD است.
نقش proxy در Next.js جدید
در نسخههای جدید Next.js، فایل middleware به مدل جدیدتر proxy تغییر مسیر داده است.
proxy قبل از رسیدن درخواست به route اجرا میشود و میتواند برای کارهایی مثل اینها استفاده شود:
- بررسی احراز هویت برای مسیرهای خاص
- redirect کردن کاربر
- rewrite کردن درخواست
- محدود کردن دسترسی به بعضی مسیرها
مثلاً:
// proxy.ts
export const config = {
matcher: '/api/:path*',
}
export function proxy(request: Request) {
// auth checks
}
اما یک نکته مهم:
نباید فقط به proxy برای امنیت endpointها تکیه کنیم.
خود Route Handler هم باید بررسیهای لازم مثل authentication و authorization را انجام دهد، مخصوصاً برای عملیات حساس مثل حذف یا ویرایش داده.
چرا در این دوره Route Handlers را انتخاب میکنیم؟
در این سری آموزشی، هدف فقط ارسال یک فرم ساده نیست. ما میخواهیم بهمرور یک ساختار واقعیتر بسازیم:
- فرم تماس
- مدیریت پروژهها
- مدیریت مقالات
- داشبورد
- اتصال به دیتابیس
- اعتبارسنجی با
Zod - عملیات
CRUD - احراز هویت
- آپلود تصویر
برای چنین مسیری، Route Handlers انتخاب بهتری هستند، چون:
- مدل استاندارد
HTTPرا یاد میگیریم - برای
CRUDبسیار شفاف هستند - با
Prismaخوب ترکیب میشوند - قابل تست هستند
- از
clientباfetchقابل استفادهاند - ساختار پروژه را برای APIهای بعدی آماده میکنند
- تفاوت
GET,POST,PATCH,DELETEرا عملی یاد میگیریم
Server Actions را کنار نمیگذاریم، اما در این مرحله انتخاب اصلی ما نیستند.
آنها را بهعنوان یک ابزار مفید میشناسیم، اما برای ساخت API و CRUD اصلی پروژه، از Route Handlers استفاده میکنیم.
جمعبندی
در این قسمت یاد گرفتیم که برای اتصال فرمها و UI به بکاند در Next.js دو مسیر مهم داریم:
Server ActionsRoute Handlers
Server Actions برای عملیات ساده و مستقیم از داخل فرم یا کامپوننت بسیار کاربردی هستند. اما برای ساخت یک API قابل توسعه، قابل تست و مناسب برای CRUD، Route Handlers انتخاب اصلی ما خواهند بود.
همچنین دیدیم که Route Handlers:
- داخل پوشه
appو با فایلroute.tsساخته میشوند - از متدهای
GET,POST,PATCH,DELETEو متدهای دیگر پشتیبانی میکنند - با
Request,Response,NextRequestوNextResponseکار میکنند - میتوانند داده را با
request.json()یاrequest.formData()بخوانند - باید داده ورودی را اعتبارسنجی کنند
- برای ساخت API در الگوی
Backend for Frontendمناسب هستند - endpoint عمومی محسوب میشوند و نیاز به رعایت امنیت دارند