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:
- Next.js catch-all route
[trpc]matches any path under/api/trpc/ fetchRequestHandlerfrom tRPC's fetch adapter processes the requestappRouterfrom@repo/apicontains all registered routerscreateContextprovides the context object to all procedures (currently empty{})- 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/-- seepackages/api/AGENTS.md - tRPC client/hooks:
packages/api-client/-- seepackages/api-client/AGENTS.md - UI components:
packages/ui/-- seepackages/ui/AGENTS.md - CMS client:
packages/cms-client/-- seepackages/cms-client/AGENTS.md - CMS config:
packages/cms-core/-- seepackages/cms-core/AGENTS.md