# apps/web-next -- Next.js 15 Reference App ## Purpose Next.js 15 reference application using App Router. Demonstrates how to consume `@repo/api-client` for tRPC data fetching, `@repo/ui` for components, and `@repo/api` for the tRPC HTTP endpoint. This is a thin app -- business logic lives in `@repo/core`, UI components live in `@repo/ui`. ## Port: 3000 ```bash pnpm dev --filter @repo/web-next # http://localhost:3000 ``` ## Key Files | File | Purpose | |---|---| | `src/app/layout.tsx` | Root layout -- wraps children with ``, sets HTML metadata | | `src/app/providers.tsx` | Client component that wraps the app with `` | | `src/app/page.tsx` | Home page (server component by default) | | `src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP endpoint using the Next.js fetch adapter | ## tRPC Endpoint Setup The file `src/app/api/trpc/[trpc]/route.ts` creates a catch-all API route that handles all tRPC requests: ```typescript import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { appRouter } from "@repo/api"; const handler = (req: Request) => fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter, createContext: () => ({}), }); export { handler as GET, handler as POST }; ``` How it works: 1. Next.js catch-all route `[trpc]` matches any path under `/api/trpc/` 2. `fetchRequestHandler` from tRPC's fetch adapter processes the request 3. `appRouter` from `@repo/api` contains all registered routers 4. `createContext` provides the context object to all procedures (currently empty `{}`) 5. Both GET (for queries) and POST (for mutations/batched queries) are exported ## Provider Setup The `` from `@repo/api-client` must wrap the entire app. Since it uses React hooks, it lives in a `"use client"` component: ```tsx // src/app/providers.tsx "use client"; import { ApiProvider } from "@repo/api-client"; export function Providers({ children }: { children: React.ReactNode }) { return {children}; } ``` ```tsx // src/app/layout.tsx import { Providers } from "./providers"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## Recipe: Adding a New Page with Data Fetching This example adds an `/articles` page that lists published articles. ### Step 1: Create the page route Create `src/app/articles/page.tsx`: ```tsx import { ArticleList } from "./article-list"; export default function ArticlesPage() { return (

Articles

); } ``` ### Step 2: Create the client component with data fetching Create `src/app/articles/article-list.tsx`: ```tsx "use client"; import { useTRPC } from "@repo/api-client"; import { useQuery } from "@tanstack/react-query"; import { Button } from "@repo/ui"; export function ArticleList() { const trpc = useTRPC(); const { data, isLoading, error } = useQuery( trpc.content.listArticles.queryOptions({ status: "published", limit: 20 }) ); if (isLoading) return

Loading articles...

; if (error) return

Error loading articles: {error.message}

; return (
    {data?.map((article) => (
  • {article.title}

  • ))}
); } ``` Key patterns: - The page component (`page.tsx`) is a server component by default -- no `"use client"` needed - Data-fetching components that use `useTRPC()` must be client components (`"use client"`) - Import UI components from `@repo/ui`, never recreate them locally ## Payload Initialization Pattern (Server-Side Local API) For server-side access to Payload CMS data (e.g., in server components, API routes, or server actions), create a Payload client initializer: ```typescript // src/lib/payload.ts import { getPayload } from "payload"; import config from "@repo/cms-core/src/payload.config"; import { createPayloadClient, type PayloadClient } from "@repo/cms-client"; let cachedClient: PayloadClient | null = null; export async function getPayloadClient(): Promise { if (cachedClient) return cachedClient; const payload = await getPayload({ config }); cachedClient = createPayloadClient({ mode: "local", payload }); return cachedClient; } ``` Usage in a server component: ```tsx // src/app/articles/page.tsx (server component) import { getPayloadClient } from "@/lib/payload"; export default async function ArticlesPage() { const client = await getPayloadClient(); const result = await client.find("articles", { where: { status: { equals: "published" } }, sort: "-publishedAt", limit: 20, }); return (

Articles

    {result.docs.map((article) => (
  • {article.title}
  • ))}
); } ``` ## Dependencies | Dependency | Purpose | |---|---| | `@repo/api` | `appRouter` for the tRPC HTTP endpoint | | `@repo/api-client` | `ApiProvider` + `useTRPC()` for client-side data fetching | | `@repo/ui` | Shared UI components (Button, Input, Label, FormField, etc.) | | `next` | Next.js 15 framework with App Router | | `react` / `react-dom` | React 19 runtime | ## Cross-References - **tRPC routers:** `packages/api/` -- see `packages/api/AGENTS.md` - **tRPC client/hooks:** `packages/api-client/` -- see `packages/api-client/AGENTS.md` - **UI components:** `packages/ui/` -- see `packages/ui/AGENTS.md` - **CMS client:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md` - **CMS config:** `packages/cms-core/` -- see `packages/cms-core/AGENTS.md`