Files
agentic-dev-template/apps/web-next/AGENTS.md

5.7 KiB

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

pnpm dev --filter @repo/web-next   # http://localhost:3000

Key Files

File Purpose
src/app/layout.tsx Root layout -- wraps children with <Providers>, sets HTML metadata
src/app/providers.tsx Client component that wraps the app with <ApiProvider trpcUrl="/api/trpc">
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:

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 <ApiProvider> from @repo/api-client must wrap the entire app. Since it uses React hooks, it lives in a "use client" component:

// src/app/providers.tsx
"use client";

import { ApiProvider } from "@repo/api-client";

export function Providers({ children }: { children: React.ReactNode }) {
  return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
}
// src/app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

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:

import { ArticleList } from "./article-list";

export default function ArticlesPage() {
  return (
    <main>
      <h1>Articles</h1>
      <ArticleList />
    </main>
  );
}

Step 2: Create the client component with data fetching

Create src/app/articles/article-list.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 <p>Loading articles...</p>;
  if (error) return <p>Error loading articles: {error.message}</p>;

  return (
    <ul>
      {data?.map((article) => (
        <li key={article.id}>
          <h2>{article.title}</h2>
          <Button variant="outline" size="sm">
            Read more
          </Button>
        </li>
      ))}
    </ul>
  );
}

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:

// 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<PayloadClient> {
  if (cachedClient) return cachedClient;

  const payload = await getPayload({ config });
  cachedClient = createPayloadClient({ mode: "local", payload });
  return cachedClient;
}

Usage in a server component:

// 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 (
    <main>
      <h1>Articles</h1>
      <ul>
        {result.docs.map((article) => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </main>
  );
}

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