MATIN MOLKARA
SERIES: آموزش Next js — PART 16 OF 28

قسمت شانزدهم - 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 Actions
  • Route 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 پشتیبانی می‌کنند:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS

اگر یک متد را تعریف نکرده باشیم و کاربر با همان متد به 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 نسخه‌های توسعه‌یافته‌ای هم دارد:

  • NextRequest
  • NextResponse

این‌ها از پکیج 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, DELETE cache نمی‌شوند
  • برای داده‌های حساس یا قابل تغییر، نباید بی‌دقت 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
  • فایل
  • تصویر
  • RSS
  • robots.txt
  • sitemap
  • webhook

مثلاً می‌توانیم یک 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 Actions
  • Route 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 عمومی محسوب می‌شوند و نیاز به رعایت امنیت دارند
// 0 comments
#Next.js#actions#اموزش

نظرات (0)

نظر خود را بنویسید