לדלג לתוכן

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"]

תרשים שמראה שלוש כתובות URL וכיצד כל אחת מהן נתפסת על ידי סוג אחר של תיקייה דינמית והערך שמתקבל בתוך params בכל מקרה

יצירת דפים סטטיים - 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 ייווצרו דינמית בבקשה הראשונה

תרשים זרימה שמראה איך generateStaticParams יוצר קובץ HTML סטטי לכל slug בזמן הבנייה ואיך slug חדש שלא נבנה מראש מרונדר בבקשה הראשונה ונשמר

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

// 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];
  // verify the token...

  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();

  // log in...
  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, // a week
  });

  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;

  // redirect from /old-page to /new-page
  if (pathname === "/old-page") {
    return NextResponse.redirect(new URL("/new-page", request.url));
  }

  // check authentication for dashboard routes
  if (pathname.startsWith("/dashboard")) {
    const token = request.cookies.get("session");
    if (!token) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

// define the routes the middleware runs on
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 רץ לפני כל בקשה ומתאים להפניות ואותנטיקציה