לדלג לתוכן

9.5 נתיבים דינמיים ונתיבי API הרצאה

נתיבים דינמיים ונתיבי API - Dynamic Routes and API Routes

בשיעור זה נלמד ליצור נתיבים דינמיים שמשתנים לפי פרמטרים, ונכיר את Route Handlers שמאפשרים ליצור API endpoints ישירות בתוך פרויקט נקסט.


נתיבים דינמיים - Dynamic Segments

נתיבים דינמיים נוצרים על ידי שימוש בסוגריים מרובעים בשם התיקייה:

סגמנט בודד - [slug]

app/
  blog/
    [slug]/
      page.tsx        →  /blog/my-first-post, /blog/hello-world
// app/blog/[slug]/page.tsx
interface PageProps {
  params: { slug: string };
}

export default async function BlogPostPage({ params }: PageProps) {
  const { slug } = params;

  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await res.json();

  return (
    <article className="max-w-3xl mx-auto p-6">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <time className="text-gray-500">
        {new Date(post.date).toLocaleDateString("he-IL")}
      </time>
      <div className="mt-6 prose">{post.content}</div>
    </article>
  );
}
  • params.slug יכיל את הערך מה-URL
  • /blog/my-post - params.slug = "my-post"
  • /blog/hello - params.slug = "hello"

פרמטרים מרובים

app/
  shop/
    [category]/
      [productId]/
        page.tsx      →  /shop/shoes/123
// app/shop/[category]/[productId]/page.tsx
interface PageProps {
  params: {
    category: string;
    productId: string;
  };
}

export default async function ProductPage({ params }: PageProps) {
  const { category, productId } = params;

  return (
    <div className="p-6">
      <p className="text-gray-500">קטגוריה: {category}</p>
      <h1 className="text-3xl font-bold">מוצר {productId}</h1>
    </div>
  );
}

נתיבים עם Catch-All

Catch-All - [...slug]

תופס את כל הסגמנטים שנותרו:

app/
  docs/
    [...slug]/
      page.tsx        →  /docs/a, /docs/a/b, /docs/a/b/c
// app/docs/[...slug]/page.tsx
interface PageProps {
  params: { slug: string[] };
}

export default function DocsPage({ params }: PageProps) {
  const { slug } = params;

  // /docs/getting-started → slug = ["getting-started"]
  // /docs/api/auth/login  → slug = ["api", "auth", "login"]

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold">תיעוד</h1>
      <p>נתיב: /{slug.join("/")}</p>
      <p>עומק: {slug.length} רמות</p>
    </div>
  );
}
  • /docs לבד לא ייתפס - חייב לפחות סגמנט אחד

Optional Catch-All - [[...slug]]

כמו catch-all אבל תופס גם את הנתיב הבסיסי:

app/
  docs/
    [[...slug]]/
      page.tsx        →  /docs, /docs/a, /docs/a/b
// app/docs/[[...slug]]/page.tsx
interface PageProps {
  params: { slug?: string[] };
}

export default function DocsPage({ params }: PageProps) {
  const { slug } = params;

  if (!slug) {
    return <h1>דף הבית של התיעוד</h1>;
  }

  return (
    <div>
      <h1>תיעוד: {slug.join(" / ")}</h1>
    </div>
  );
}
  • /docs - slug = undefined
  • /docs/intro - slug = ["intro"]
  • /docs/api/users - slug = ["api", "users"]

יצירת דפים סטטיים - generateStaticParams

כשרוצים לבנות דפים דינמיים כסטטיים בזמן ה-build:

// app/blog/[slug]/page.tsx
import { prisma } from "@/lib/prisma";

export async function generateStaticParams() {
  const posts = await prisma.post.findMany({
    select: { slug: true },
  });

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug },
  });

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}
  • generateStaticParams מחזיר מערך של כל הפרמטרים האפשריים
  • נקסט בונה דף סטטי לכל אחד בזמן ה-build
  • דפים חדשים שלא היו ב-build ייווצרו דינמית בבקשה הראשונה

שימוש עם פרמטרים מרובים

// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
  const products = await prisma.product.findMany({
    include: { category: true },
  });

  return products.map((product) => ({
    category: product.category.slug,
    productId: product.id.toString(),
  }));
}

נתיבי API - Route Handlers

Route Handlers מאפשרים ליצור API endpoints בתוך הפרויקט:

app/
  api/
    users/
      route.ts          →  /api/users
    posts/
      [id]/
        route.ts        →  /api/posts/123

בקשות GET

// app/api/users/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET() {
  const users = await prisma.user.findMany();
  return NextResponse.json(users);
}

בקשות POST

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { name, email } = body;

  if (!name || !email) {
    return NextResponse.json(
      { error: "שם ואימייל הם שדות חובה" },
      { status: 400 }
    );
  }

  const user = await prisma.user.create({
    data: { name, email },
  });

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

בקשות PUT ו-DELETE

// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await prisma.post.findUnique({
    where: { id: parseInt(params.id) },
  });

  if (!post) {
    return NextResponse.json({ error: "פוסט לא נמצא" }, { status: 404 });
  }

  return NextResponse.json(post);
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await request.json();

  const post = await prisma.post.update({
    where: { id: parseInt(params.id) },
    data: body,
  });

  return NextResponse.json(post);
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await prisma.post.delete({
    where: { id: parseInt(params.id) },
  });

  return NextResponse.json({ message: "נמחק בהצלחה" });
}

עבודה עם Request ו-Response

קריאת Query Parameters

// app/api/search/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get("q");
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "10");

  // /api/search?q=nextjs&page=2&limit=20

  const results = await prisma.post.findMany({
    where: {
      title: { contains: query || "" },
    },
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({
    results,
    page,
    totalPages: Math.ceil(results.length / limit),
  });
}

עבודה עם Headers

// app/api/protected/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const authHeader = request.headers.get("authorization");

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return NextResponse.json(
      { error: "לא מורשה" },
      { status: 401 }
    );
  }

  const token = authHeader.split(" ")[1];
  // אימות הטוקן...

  return NextResponse.json({ message: "תוכן מוגן" });
}

עבודה עם Cookies

// app/api/auth/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
  const body = await request.json();

  // התחברות...
  const token = "jwt-token-here";

  const response = NextResponse.json({ success: true });
  response.cookies.set("session", token, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // שבוע
  });

  return response;
}

export async function GET() {
  const cookieStore = cookies();
  const session = cookieStore.get("session");

  if (!session) {
    return NextResponse.json({ error: "לא מחובר" }, { status: 401 });
  }

  return NextResponse.json({ user: "ישראל" });
}

מידלוור בסיסי - Middleware Basics

קובץ middleware.ts ברמה הראשית של הפרויקט:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // הפניה מ-/old-page ל-/new-page
  if (pathname === "/old-page") {
    return NextResponse.redirect(new URL("/new-page", request.url));
  }

  // בדיקת אותנטיקציה לנתיבי דשבורד
  if (pathname.startsWith("/dashboard")) {
    const token = request.cookies.get("session");
    if (!token) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

// הגדרת נתיבים שהמידלוור רץ עליהם
export const config = {
  matcher: ["/dashboard/:path*", "/old-page"],
};
  • המידלוור רץ לפני כל בקשה (לפני Route Handlers ודפים)
  • אפשר להגדיר matcher כדי שירוץ רק על נתיבים מסוימים
  • שימושי להפניות, אותנטיקציה, הוספת headers

דוגמה מלאה - API לניהול מוצרים

// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const category = searchParams.get("category");
  const sort = searchParams.get("sort") || "createdAt";
  const order = searchParams.get("order") || "desc";

  const products = await prisma.product.findMany({
    where: category ? { category } : undefined,
    orderBy: { [sort]: order },
    include: { reviews: { take: 3 } },
  });

  return NextResponse.json(products);
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { name, price, description, category } = body;

    if (!name || !price) {
      return NextResponse.json(
        { error: "שם ומחיר הם שדות חובה" },
        { status: 400 }
      );
    }

    const product = await prisma.product.create({
      data: { name, price, description, category },
    });

    return NextResponse.json(product, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "שגיאה ביצירת המוצר" },
      { status: 500 }
    );
  }
}
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const product = await prisma.product.findUnique({
    where: { id: parseInt(params.id) },
    include: {
      reviews: {
        include: { user: { select: { name: true } } },
      },
    },
  });

  if (!product) {
    return NextResponse.json({ error: "מוצר לא נמצא" }, { status: 404 });
  }

  return NextResponse.json(product);
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json();

    const product = await prisma.product.update({
      where: { id: parseInt(params.id) },
      data: body,
    });

    return NextResponse.json(product);
  } catch (error) {
    return NextResponse.json({ error: "מוצר לא נמצא" }, { status: 404 });
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    await prisma.product.delete({
      where: { id: parseInt(params.id) },
    });

    return NextResponse.json({ message: "המוצר נמחק" });
  } catch (error) {
    return NextResponse.json({ error: "מוצר לא נמצא" }, { status: 404 });
  }
}

סיכום

  • נתיבים דינמיים נוצרים עם [slug] בשם התיקייה
  • [...slug] תופס כל הסגמנטים, [[...slug]] תופס גם את הנתיב הריק
  • generateStaticParams מאפשר בנייה סטטית של דפים דינמיים
  • Route Handlers (route.ts) מגדירים API endpoints עם GET, POST, PUT, DELETE
  • עובדים עם NextRequest ו-NextResponse לקריאת query params, headers ו-cookies
  • Middleware רץ לפני כל בקשה ומתאים להפניות ואותנטיקציה