Route Handlers ב-Next.js 16: מדריך מלא ל-API ב-App Router

מדריך מעשי ל-Route Handlers ב-Next.js 16: GET/POST/PUT/DELETE, params כ-Promise, caching דינמי, סטרימינג, Edge runtime, CORS ואימות עם Auth.js v5.

Next.js 16 Route Handlers Guide (2026)

עודכן: 3 ביוני 2026

Route Handlers ב-Next.js 16 הם פונקציות שרת שמוגדרות בקובץ route.ts בתוך תיקיית app/, ומאפשרות לבנות endpoints מסוג REST מלאים באמצעות Web API סטנדרטיים של Request ו-Response. הן החליפו לחלוטין את pages/api ב-App Router, רצות כברירת מחדל ללא cache, ותומכות הן ב-Node runtime והן ב-Edge runtime. במדריך הזה אני אעבור איתכם על כל מה שצריך כדי לבנות API production-ready ב-Next.js 16, כולל POST/PUT/DELETE, סטרימינג, אימות, CORS וטיפול בשגיאות.

  • Route Handlers מוגדרים בקובץ app/.../route.ts ומייצאים פונקציה לכל HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
  • החל מ-Next.js 15.0 ו-16 כל ה-Route Handlers אינם נשמרים ב-cache כברירת מחדל. כדי לאפשר caching יש להגדיר export const dynamic = 'force-static' או להשתמש ב-'use cache'.
  • הם מקבלים אובייקט Request סטנדרטי של Web API ומחזירים Response או NextResponse, כך שאותו קוד עובד גם ב-Edge runtime.
  • פרמטרים דינמיים מועברים דרך הארגומנט השני { params }, כאשר params הוא Promise החל מ-Next.js 15.
  • סטרימינג מבוצע על-ידי החזרת ReadableStream, מה שמתאים לתגובות LLM, SSE ופלטים ארוכים.
  • ההבדל העיקרי מ-Server Actions: Route Handlers נקראים מ-clients חיצוניים (mobile, webhooks, third-party), בעוד Server Actions נועדו לקריאה מתוך React components בלבד.

מה הם Route Handlers ב-Next.js 16?

Route Handlers הם המנגנון של App Router ליצירת endpoints בצד השרת. במקום הגישה הישנה של pages/api/users.ts שייצאה פונקציית default, ב-Next.js 16 מגדירים קובץ בשם route.ts (או route.js) בתוך כל תיקייה תחת app/, ומייצאים פונקציה נפרדת לכל HTTP method. הקובץ הזה לא יכול לחיות באותה תיקייה כמו page.tsx. תיקייה מכילה או עמוד או handler, לא שניהם.

השינוי הארכיטקטוני המרכזי הוא שה-handler מקבל אובייקט Request סטנדרטי של Web API במקום ה-NextApiRequest/NextApiResponse הישנים. המשמעות היא שהקוד שלכם נייד, ואותו handler יכול לרוץ ב-Node, ב-Edge, ואפילו ב-Workers של ספקי ענן אחרים בשינויים מינימליים. ה-תיעוד הרשמי של Route Handlers מתעד את כל ה-methods הנתמכים וההתנהגות שלהם.

ב-Next.js 16 בפרט יש שלושה שינויים שכדאי להכיר: params ו-searchParams הפכו ל-Promises אסינכרוניים, ה-cache ברירת המחדל הוסר (כל handler הוא דינמי אלא אם תגדירו אחרת), והאינטגרציה עם Cache Components החדשה דרך 'use cache' מאפשרת control גרעיני יותר על מה שנשמר. אם אתם מגיעים מ-Next.js 13 או 14, חלק מהקוד שלכם ידרוש התאמה.

בניית Route Handler ראשון: GET ו-POST

נתחיל עם דוגמה מינימלית. נניח שאני בונה API לניהול משימות. אצור את הקובץ app/api/tasks/route.ts:

// app/api/tasks/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

// GET /api/tasks?status=open
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const status = searchParams.get('status') ?? 'open';

  const tasks = await db.task.findMany({
    where: { status },
    orderBy: { createdAt: 'desc' },
  });

  return NextResponse.json({ tasks }, { status: 200 });
}

// POST /api/tasks
export async function POST(request: Request) {
  const body = await request.json();

  if (!body.title || typeof body.title !== 'string') {
    return NextResponse.json(
      { error: 'title is required' },
      { status: 400 },
    );
  }

  const task = await db.task.create({
    data: { title: body.title, status: 'open' },
  });

  return NextResponse.json({ task }, { status: 201 });
}

שימו לב לכמה דברים. אני קורא ל-request.json() כדי לפרסר את ה-body, וזו הדרך הסטנדרטית של Fetch API, ולא נדרשת ספרייה כמו body-parser. את ה-query string אני קורא דרך new URL(request.url).searchParams, מה שעובד זהה גם ב-Node וגם ב-Edge. הערך המוחזר מ-NextResponse.json הוא Response תקני, ולכן אפשר להחזיר גם new Response(...) רגיל אם רוצים שליטה מלאה ב-headers.

פרמטרים דינמיים ו-params כ-Promise

בשביל endpoint כמו /api/tasks/[id], יוצרים את הקובץ app/api/tasks/[id]/route.ts. ב-Next.js 16 הפרמטרים מועברים דרך הארגומנט השני, אבל ה-params הוא Promise. זה שינוי שבוצע ב-Next.js 15 כדי לתמוך ב-Partial Prerendering וב-streaming של context. אם אתם משדרגים מ-Next.js 13/14, זה אחד השינויים הראשונים שתפגשו:

// app/api/tasks/[id]/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

type Context = {
  params: Promise<{ id: string }>;
};

export async function GET(request: Request, ctx: Context) {
  const { id } = await ctx.params; // חובה await ב-Next.js 15+

  const task = await db.task.findUnique({ where: { id } });

  if (!task) {
    return NextResponse.json({ error: 'not found' }, { status: 404 });
  }
  return NextResponse.json({ task });
}

export async function PATCH(request: Request, ctx: Context) {
  const { id } = await ctx.params;
  const updates = await request.json();

  const task = await db.task.update({ where: { id }, data: updates });
  return NextResponse.json({ task });
}

export async function DELETE(request: Request, ctx: Context) {
  const { id } = await ctx.params;
  await db.task.delete({ where: { id } });
  return new Response(null, { status: 204 });
}

שגיאה נפוצה שאני רואה אצל מפתחים שמשדרגים: שימוש ב-ctx.params.id ישירות בלי await. ב-TypeScript זה ייתפס מיד, אבל בקוד JS פשוט תקבלו undefined בלי הסבר ברור. אם אתם עוברים פרויקט גדול, הרצה של npx @next/codemod@latest next-async-request-api . תחליף את כל הקריאות אוטומטית.

האם Route Handlers נשמרים ב-cache כברירת מחדל?

לא. החל מ-Next.js 15.0 שוחרר באוקטובר 2024, ובהמשך ל-Next.js 16, כל Route Handlers הם דינמיים כברירת מחדל. בעבר GET handlers נשמרו ב-cache באופן אגרסיבי, מה שגרם להמון בילבול כשמשתמשים החזירו נתונים ישנים מ-database. הצוות החליט להפוך את ההתנהגות לבטוחה כברירת מחדל, ולחייב opt-in מפורש ל-caching.

יש שלוש דרכים להחזיר caching לדינמיקה הזו:

// אופציה 1: השבתה מלאה של דינמיקה, רק GET
// app/api/static-list/route.ts
export const dynamic = 'force-static';
export const revalidate = 3600; // שעה

export async function GET() {
  const items = await fetch('https://api.example.com/items').then(r => r.json());
  return Response.json(items);
}
// אופציה 2: Cache Components החדש ב-Next.js 16
// app/api/products/route.ts
import { unstable_cache as cache } from 'next/cache';

const getProducts = cache(
  async (category: string) => {
    return db.product.findMany({ where: { category } });
  },
  ['products-by-category'],
  { revalidate: 60, tags: ['products'] },
);

export async function GET(request: Request) {
  const category = new URL(request.url).searchParams.get('category') ?? 'all';
  const products = await getProducts(category);
  return Response.json(products);
}

עבור endpoints שמשנים נתונים (POST/PUT/PATCH/DELETE) צריך לקרוא ל-revalidateTag או revalidatePath אחרי הכתיבה, אחרת ה-cache של ה-GET ימשיך להחזיר ערכים ישנים. תוכלו לקרוא יותר על דפוסי caching במדריך מעשי ל-Cache Components ב-Next.js 16.

מה ההבדל בין Route Handlers ל-Server Actions?

זו שאלה שאני נשאל הכי הרבה. שניהם רצים על השרת, שניהם יכולים לגשת ל-database, אבל הם פותרים בעיות שונות. הטבלה למטה מסכמת את ההבדלים העיקריים:

קריטריוןRoute HandlersServer Actions
קריאה מ-clients חיצונייםכן (mobile, webhooks, curl)לא, רק מתוך React
פורמט תגובהJSON, XML, binary, streamJSON serializable בלבד
שיטות HTTPGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONSPOST תמיד (מוסתר)
Progressive enhancementלאכן, עובד בלי JS
קלות שימוש בטפסיםנדרש fetch ידנימובנה ב-<form action>
Type safety client→serverנדרשת ספרייה (zod, tRPC)מובנה דרך imports
שימוש טיפוסיAPI ציבורי, webhooks, OAuth callbacksמוטציות UI פנימיות

בקצרה: אם הלקוח שלכם הוא browser שמריץ את אותה אפליקציית Next.js, Server Actions יחסכו לכם boilerplate. אם הלקוח הוא משהו אחר (אפליקציית React Native, webhook של Stripe, מערכת חיצונית), Route Handlers הם הבחירה. למעשה, פרויקטים רבים משתמשים בשניהם: Server Actions עבור הזרימה הפנימית, ו-Route Handlers בתיקייה app/api/v1/ עבור צרכנים חיצוניים. למדריך המלא ל-Server Actions ב-Next.js 16 יש פירוט עמוק על הצד השני של המשוואה.

סטרימינג תגובות עם ReadableStream

אחת הסיבות החזקות לבחור ב-Route Handler על פני Server Action היא סטרימינג. מודלי LLM, server-sent events ו-large file downloads דורשים לשלוח נתונים ב-chunks בלי לחכות שהכול יסתיים. כיוון ש-Route Handlers מחזירים Response סטנדרטי של Web API, אתם פשוט מחזירים ReadableStream:

// app/api/chat/route.ts
import { openai } from '@/lib/openai';

export async function POST(request: Request) {
  const { messages } = await request.json();

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages,
    stream: true,
  });

  // ממירים את ה-async iterable ל-ReadableStream
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of completion) {
          const text = chunk.choices[0]?.delta?.content ?? '';
          if (text) controller.enqueue(encoder.encode(text));
        }
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      'X-Accel-Buffering': 'no',
    },
  });
}

ה-header X-Accel-Buffering: no חשוב כשרצים מאחורי Nginx או reverse proxy אחר. בלעדיו, ה-proxy יבאפר את התגובה כולה ויאפס את היתרון של streaming. ה-תיעוד של ReadableStream ב-MDN מסביר את ה-API במלואו אם רוצים להעמיק.

איך מריצים Route Handler ב-Edge runtime?

Edge runtime מאפשר לקוד לרוץ קרוב יותר למשתמש בנקודות PoP של Vercel/Cloudflare. ההגדרה היא שורה אחת בראש הקובץ:

// app/api/geo/route.ts
export const runtime = 'edge';

export async function GET(request: Request) {
  // ב-Vercel: x-vercel-ip-country, x-vercel-ip-city
  const country = request.headers.get('x-vercel-ip-country') ?? 'unknown';
  const city = request.headers.get('x-vercel-ip-city') ?? 'unknown';

  return Response.json({ country, city, ts: Date.now() });
}

ה-tradeoffs: Edge runtime לא תומך ב-Node APIs מלאים. אין לכם fs, אין net, ספריות native לא יעבדו, ויש מגבלת זיכרון של כ-128MB. ספריות database רבות (כמו pg הקלאסי) לא ירוצו, וצריך drivers שמותאמים ל-Edge כמו @neondatabase/serverless או @vercel/postgres. אבל עבור endpoints קלים, גיאו-לוקיישן, A/B testing ו-feature flags, Edge מספק latency של עשרות מילישניות במקום מאות.

איך מוסיפים CORS ל-Route Handlers?

אם ה-API שלכם נקרא מ-origin אחר (sub-domain, mobile app, חברה שנייה), הדפדפן יחסום את הבקשות אלא אם תוסיפו headers של CORS. Next.js לא עושה את זה אוטומטית. הדפוס הסטנדרטי הוא לטפל ב-preflight OPTIONS ולצרף headers לכל תגובה:

// app/api/public/route.ts
const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://admin.example.com',
];

function corsHeaders(origin: string | null) {
  const allow = origin && ALLOWED_ORIGINS.includes(origin) ? origin : '';
  return {
    'Access-Control-Allow-Origin': allow,
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400',
    'Vary': 'Origin',
  };
}

export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 204,
    headers: corsHeaders(request.headers.get('origin')),
  });
}

export async function GET(request: Request) {
  const data = { items: [] };
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
      ...corsHeaders(request.headers.get('origin')),
    },
  });
}

שני דברים חשובים: אל תחזירו Access-Control-Allow-Origin: * ב-endpoints שדורשים cookies או credentials, כי הדפדפן ידחה את התגובה. ושימו Vary: Origin כדי ש-CDN לא יחזיר תגובה ממקור אחד למקור אחר. אם ה-CORS חוזר על עצמו בהרבה endpoints, שקלו להעביר אותו ל-proxy.ts (מה שנקרא בעבר middleware) כדי להחיל אותו על כל /api/* במקום אחד.

אימות והגנה על endpoints

Route Handlers הם endpoints ציבוריים כברירת מחדל. כל מי שיודע את ה-URL יכול לקרוא להם. יש שלוש שכבות הגנה שאני ממליץ להחיל בייצור: אימות (מי הוא המשתמש?), הרשאות (מה הוא רשאי לעשות?), ו-rate limiting (כמה פעמים בשנייה?).

// app/api/me/route.ts
import { NextResponse } from 'next/server';
import { auth } from '@/auth'; // Auth.js v5
import { rateLimit } from '@/lib/rate-limit';

export async function GET(request: Request) {
  // 1. rate limit לפי IP
  const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown';
  const { success } = await rateLimit.check(ip, 60); // 60 בקשות בדקה
  if (!success) {
    return NextResponse.json({ error: 'rate limited' }, { status: 429 });
  }

  // 2. אימות
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }

  // 3. הרשאות
  if (session.user.role !== 'admin' && session.user.role !== 'user') {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  }

  return NextResponse.json({ user: session.user });
}

אם אתם משתמשים ב-Auth.js v5, ה-auth() helper עובד ישירות בתוך Route Handler ולא דורש שום setup נוסף. שדרגתי לאחרונה פרויקט גדול וגיליתי שזה אחד הדפוסים שהפכו את ה-API שלנו לרבה יותר נקי. למי שעוד לא הגדיר אימות בפרויקט שלו, יש לנו מדריך מלא לאימות ב-Next.js 16 עם Auth.js v5 שמכסה את כל ההגדרה.

עבור webhooks (Stripe, GitHub, Resend) האימות נראה אחרת. אתם בודקים HMAC signature מ-header ולא session. השאירו את ה-route הזה תחת path נפרד כמו /api/webhooks/stripe כדי שיהיה ברור שזה endpoint ציבורי שעובר אימות אחר.

טיפול בשגיאות וקודי סטטוס

שגיאות לא מטופלות ב-Route Handler מחזירות תגובה גנרית של 500. זה בסדר ל-prototype, גרוע לייצור. הדפוס שאני מחיל בכל פרויקט הוא wrapper דק שלוכד throws, מבדיל בין שגיאות צפויות (ולידציה, not found) לבלתי צפויות (DB down), ומחזיר JSON עקבי:

// lib/api-error.ts
export class ApiError extends Error {
  constructor(public status: number, public code: string, message: string) {
    super(message);
  }
}

export function handleApiError(e: unknown) {
  if (e instanceof ApiError) {
    return Response.json(
      { error: { code: e.code, message: e.message } },
      { status: e.status },
    );
  }
  console.error('[unhandled]', e); // נשלח אוטומטית ל-Sentry
  return Response.json(
    { error: { code: 'internal', message: 'something went wrong' } },
    { status: 500 },
  );
}

// app/api/tasks/[id]/route.ts
export async function GET(request: Request, ctx: { params: Promise<{ id: string }> }) {
  try {
    const { id } = await ctx.params;
    const task = await db.task.findUnique({ where: { id } });
    if (!task) throw new ApiError(404, 'task_not_found', `task ${id} not found`);
    return Response.json({ task });
  } catch (e) {
    return handleApiError(e);
  }
}

הקפידו על קודי הסטטוס הנכונים: 200 לקריאה מוצלחת, 201 ליצירה, 204 למחיקה בלי גוף, 400 לולידציה, 401 לאימות חסר, 403 להרשאה חסרה, 404 ל-not found, 409 ל-conflict (לדוגמה אימייל קיים), 422 ל-unprocessable entity, ו-429 ל-rate limit. צרכנים של ה-API שלכם (במיוחד client-side libraries כמו TanStack Query) מסתמכים על הקודים האלה כדי להפעיל retry logic או להציג הודעות שגיאה מתאימות.

שאלות נפוצות

האם אפשר להחזיר HTML מ-Route Handler?

כן. החזירו new Response(html, { headers: { 'Content-Type': 'text/html' } }). זה שימושי ל-RSS feeds, sitemap.xml, או עמודי OAuth redirect. עם זאת, עבור עמודי UI רגילים השתמשו ב-page.tsx. Route Handler לא מקבל את היתרונות של React Server Components ושל ה-streaming של App Router.

למה ה-Route Handler שלי לא משתנה אחרי deploy?

זה כמעט תמיד caching ב-CDN. בדקו את ה-header Cache-Control שאתם מחזירים, ובדקו ב-Vercel את ה-tab "Functions" אם ה-endpoint סומן כ-static. הוסיפו export const dynamic = 'force-dynamic' לראש הקובץ כדי לכבות caching לחלוטין בזמן debug.

איך מעלים קבצים דרך Route Handler?

קוראים ל-const form = await request.formData() וניגשים ל-form.get('file') as File. לאחר מכן await file.arrayBuffer() מחזיר את התוכן. ב-Edge runtime יש מגבלת גודל של 4.5MB ב-Vercel; לקבצים גדולים יותר השתמשו ב-presigned URL ישירות ל-S3/R2.

האם Route Handlers תומכים ב-WebSocket?

לא ישירות. Next.js לא חושף upgrade של HTTP ל-WebSocket. עבור real-time השתמשו ב-Server-Sent Events דרך ReadableStream (תואם ל-Route Handlers), או בשירות חיצוני כמו Pusher, Ably או Supabase Realtime שמתחבר ב-WebSocket ישירות מהדפדפן.

מה קורה כשיש גם page.tsx וגם route.ts באותה תיקייה?

Next.js יזרוק שגיאת build. שני הקבצים מתחרים על אותו URL. אם אתם צריכים גם API וגם עמוד, פתחו תיקייה ייעודית ל-API: app/dashboard/page.tsx לעמוד ו-app/api/dashboard/route.ts ל-API. זוהי אחת מהשגיאות הכי נפוצות במעבר מ-pages router.

Editorial Team
אודות הכותב Editorial Team

Our team of expert writers and editors.