Урок 10

Практика: типичный роутинг

Собрали структуру роутинга для реального проекта.

Структура

app/
├── layout.tsx
├── page.tsx                    → /
├── globals.css
├── not-found.tsx
│
├── (auth)/
│   ├── layout.tsx              ← без шапки
│   ├── login/page.tsx          → /login
│   └── register/page.tsx       → /register
│
├── (main)/
│   ├── layout.tsx              ← с шапкой
│   ├── about/page.tsx          → /about
│   ├── pricing/page.tsx        → /pricing
│   └── contact/page.tsx        → /contact
│
├── (app)/
│   ├── layout.tsx              ← с сайдбаром, защита
│   ├── dashboard/
│   │   ├── page.tsx            → /dashboard
│   │   └── loading.tsx
│   ├── posts/
│   │   ├── page.tsx            → /posts
│   │   ├── loading.tsx
│   │   ├── new/page.tsx        → /posts/new
│   │   └── [id]/
│   │       ├── page.tsx        → /posts/1
│   │       ├── loading.tsx
│   │       ├── not-found.tsx
│   │       └── edit/page.tsx   → /posts/1/edit
│   └── settings/
│       └── page.tsx            → /settings
│
├── blog/
│   ├── page.tsx                → /blog
│   ├── [slug]/
│   │   └── page.tsx            → /blog/my-post
│   └── category/
│       └── [category]/
│           └── page.tsx        → /blog/category/tech
│
└── api/
    ├── posts/route.ts          → /api/posts
    └── posts/[id]/route.ts     → /api/posts/1

Root layout

// app/layout.tsx
export default function RootLayout({ children }) {
    return (
        <html lang="ru">
            <body>{children}</body>
        </html>
    );
}

Layout аутентификации

// app/(auth)/layout.tsx
export default function AuthLayout({ children }) {
    return (
        <div className="auth-layout">
            <div className="auth-card">{children}</div>
        </div>
    );
}

Layout приложения

// app/(app)/layout.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function AppLayout({ children }) {
    const session = await auth();
    if (!session) redirect('/login');

    return (
        <div className="app-layout">
            <Sidebar user={session.user} />
            <main>{children}</main>
        </div>
    );
}

Список постов

// app/(app)/posts/page.tsx
export default async function Posts({ searchParams }) {
    const { page = '1', search = '' } = await searchParams;

    const { data: posts, meta } = await getPosts({
        page: Number(page),
        search,
    });

    return (
        <>
            <PostFilters search={search} />
            <PostList posts={posts} />
            <Pagination meta={meta} />
        </>
    );
}

Детальная страница

// app/(app)/posts/[id]/page.tsx
import { notFound } from 'next/navigation';

export async function generateMetadata({ params }) {
    const { id } = await params;
    const post = await getPost(id);
    return post ? { title: post.title } : {};
}

export default async function Post({ params }) {
    const { id } = await params;
    const post = await getPost(id);

    if (!post) notFound();

    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
            <Link href={`/posts/${id}/edit`}>Редактировать</Link>
        </article>
    );
}

Блог со slug

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
    const posts = await getAllPosts();
    return posts.map((p) => ({ slug: p.slug }));
}

export default async function Post({ params }) {
    const { slug } = await params;
    const post = await getPostBySlug(slug);

    if (!post) notFound();

    return <article>{post.content}</article>;
}

Категории блога

app/blog/category/[category]/page.tsx
  • /blog/category/tech
  • /blog/category/life

API-роут

// app/api/posts/route.ts
export async function GET(request: Request) {
    const { searchParams } = new URL(request.url);
    const page = Number(searchParams.get('page') ?? 1);

    const posts = await db.post.findMany({
        skip: (page - 1) * 10,
        take: 10,
    });

    return Response.json(posts);
}

export async function POST(request: Request) {
    const body = await request.json();
    const post = await db.post.create({ data: body });
    return Response.json(post, { status: 201 });
}

Middleware

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request) {
    const token = request.cookies.get('token')?.value;

    if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
        return NextResponse.redirect(new URL('/login', request.url));
    }

    return NextResponse.next();
}

export const config = {
    matcher: ['/dashboard/:path*', '/posts/new'],
};

Итоги

  • Группы для разных layouts
  • (auth) без шапки, (app) с сайдбаром
  • Динамические [id] и [slug]
  • generateStaticParams для блога
  • API-роуты в app/api/
  • Middleware для защиты