# Plan 4: API Layer + App Shells — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implement tRPC routers (`@repo/api`), shared React Query hooks (`@repo/api-client`), and both app shells (`apps/web-next` with Next.js 15, `apps/web-tanstack` with TanStack Start) — completing the full data flow from UI to core. **Architecture:** `@repo/api` defines the tRPC router that calls controllers from `@repo/core`. `@repo/api-client` provides a framework-agnostic React Query provider and typed hooks. Each app hosts its own tRPC HTTP endpoint and wraps with the shared provider. Both apps use identical hooks. **Tech Stack:** tRPC v11, @trpc/tanstack-react-query, TanStack Query v5, Next.js 15 (App Router), TanStack Start (Vite-based), Zustand --- ## File Map ### packages/api | File | Responsibility | |---|---| | `packages/api/package.json` | tRPC server deps | | `packages/api/src/trpc.ts` | tRPC init, context, middleware | | `packages/api/src/router/auth.router.ts` | Auth procedures | | `packages/api/src/router/content.router.ts` | Content procedures | | `packages/api/src/router/index.ts` | Root appRouter | | `packages/api/src/index.ts` | Exports AppRouter type | ### packages/api-client | File | Responsibility | |---|---| | `packages/api-client/package.json` | tRPC client + React Query deps | | `packages/api-client/src/trpc.ts` | createTRPCReact instance | | `packages/api-client/src/query-client.ts` | Shared QueryClient factory | | `packages/api-client/src/provider.tsx` | ApiProvider component | | `packages/api-client/src/index.ts` | Exports provider + trpc | ### apps/web-next | File | Responsibility | |---|---| | `apps/web-next/package.json` | Next.js 15 + deps | | `apps/web-next/next.config.mjs` | Next.js config | | `apps/web-next/src/app/layout.tsx` | Root layout with ApiProvider | | `apps/web-next/src/app/page.tsx` | Home page | | `apps/web-next/src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP handler | | `apps/web-next/src/lib/payload.ts` | Payload instance initialization | ### apps/web-tanstack | File | Responsibility | |---|---| | `apps/web-tanstack/package.json` | TanStack Start + deps | | `apps/web-tanstack/vite.config.ts` | Vite + TanStack Start plugin | | `apps/web-tanstack/src/router.tsx` | TanStack Router config | | `apps/web-tanstack/src/routes/__root.tsx` | Root layout with ApiProvider | | `apps/web-tanstack/src/routes/index.tsx` | Home page | | `apps/web-tanstack/src/lib/payload.ts` | Payload instance initialization | --- ### Task 1: packages/api — tRPC routers **Files:** - Modify: `packages/api/package.json` - Modify: `packages/api/tsconfig.json` - Create: `packages/api/src/trpc.ts` - Create: `packages/api/src/router/auth.router.ts` - Create: `packages/api/src/router/content.router.ts` - Create: `packages/api/src/router/index.ts` - Modify: `packages/api/src/index.ts` - [ ] **Step 1: Update packages/api/package.json** ```json { "name": "@repo/api", "private": true, "version": "0.0.0", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { "build": "tsc --noEmit", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/core": "workspace:*", "@trpc/server": "^11.1.0", "zod": "^3.24.0" }, "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@types/node": "^22.0.0" } } ``` - [ ] **Step 2: Update packages/api/tsconfig.json** ```json { "extends": "@repo/typescript-config/base.json", "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"], "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] } ``` - [ ] **Step 3: Create src/trpc.ts** ```typescript import { initTRPC } from "@trpc/server"; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` - [ ] **Step 4: Create src/router/auth.router.ts** ```typescript import { z } from "zod"; import { router, publicProcedure } from "../trpc.js"; import { signInController, signUpController, signOutController, } from "@repo/core"; export const authRouter = router({ signIn: publicProcedure .input( z.object({ username: z.string().min(3).max(31), password: z.string().min(6).max(255), }) ) .mutation(async ({ input }) => { return await signInController(input); }), signUp: publicProcedure .input( z.object({ username: z.string().min(3).max(31), password: z.string().min(6).max(255), confirmPassword: z.string().min(6).max(255), }) ) .mutation(async ({ input }) => { return await signUpController(input); }), signOut: publicProcedure .input(z.object({ sessionId: z.string() })) .mutation(async ({ input }) => { return await signOutController(input.sessionId); }), }); ``` - [ ] **Step 5: Create src/router/content.router.ts** ```typescript import { z } from "zod"; import { router, publicProcedure } from "../trpc.js"; import { createArticleController, getArticlesController } from "@repo/core"; export const contentRouter = router({ listArticles: publicProcedure .input( z .object({ status: z.string().optional(), authorId: z.string().optional(), limit: z.number().optional(), offset: z.number().optional(), }) .optional() ) .query(async ({ input }) => { return await getArticlesController(input ?? {}); }), createArticle: publicProcedure .input( z.object({ title: z.string().min(1).max(255), content: z.string(), authorId: z.string(), slug: z.string().optional(), }) ) .mutation(async ({ input }) => { return await createArticleController(input); }), }); ``` - [ ] **Step 6: Create src/router/index.ts** ```typescript import { router } from "../trpc.js"; import { authRouter } from "./auth.router.js"; import { contentRouter } from "./content.router.js"; export const appRouter = router({ auth: authRouter, content: contentRouter, }); export type AppRouter = typeof appRouter; ``` - [ ] **Step 7: Update src/index.ts** ```typescript export { appRouter, type AppRouter } from "./router/index.js"; ``` - [ ] **Step 8: Run pnpm install and commit** Run: `pnpm install` ```bash git add packages/api/ pnpm-lock.yaml git commit -m "feat(api): add tRPC routers (auth + content) calling core controllers" ``` --- ### Task 2: packages/api-client — shared React Query hooks + provider **Files:** - Modify: `packages/api-client/package.json` - Modify: `packages/api-client/tsconfig.json` - Create: `packages/api-client/src/trpc.ts` - Create: `packages/api-client/src/query-client.ts` - Create: `packages/api-client/src/provider.tsx` - Modify: `packages/api-client/src/index.ts` - [ ] **Step 1: Update packages/api-client/package.json** ```json { "name": "@repo/api-client", "private": true, "version": "0.0.0", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { "build": "tsc --noEmit", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/api": "workspace:*", "@trpc/client": "^11.1.0", "@trpc/tanstack-react-query": "^11.1.0", "@tanstack/react-query": "^5.75.0", "react": "^19.0.0" }, "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@types/react": "^19.0.0" } } ``` - [ ] **Step 2: Update tsconfig.json** ```json { "extends": "@repo/typescript-config/react-library.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules", "dist"] } ``` - [ ] **Step 3: Create src/trpc.ts** ```typescript import { createTRPCContext } from "@trpc/tanstack-react-query"; import type { AppRouter } from "@repo/api"; export const { TRPCProvider, useTRPC } = createTRPCContext(); ``` - [ ] **Step 4: Create src/query-client.ts** ```typescript import { QueryClient } from "@tanstack/react-query"; let clientQueryClient: QueryClient | undefined; export function getQueryClient(): QueryClient { if (typeof window === "undefined") { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000 }, }, }); } if (!clientQueryClient) { clientQueryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000 }, }, }); } return clientQueryClient; } ``` - [ ] **Step 5: Create src/provider.tsx** ```tsx "use client"; import { QueryClientProvider } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; import type { AppRouter } from "@repo/api"; import { TRPCProvider } from "./trpc.js"; import { getQueryClient } from "./query-client.js"; export function ApiProvider({ children, trpcUrl, }: { children: React.ReactNode; trpcUrl: string; }) { const queryClient = getQueryClient(); const trpcClient = createTRPCClient({ links: [httpBatchLink({ url: trpcUrl })], }); return ( {children} ); } ``` - [ ] **Step 6: Update src/index.ts** ```typescript export { ApiProvider } from "./provider.js"; export { useTRPC } from "./trpc.js"; export { getQueryClient } from "./query-client.js"; ``` - [ ] **Step 7: Run pnpm install and commit** Run: `pnpm install` ```bash git add packages/api-client/ pnpm-lock.yaml git commit -m "feat(api-client): add tRPC React Query provider and shared hooks" ``` --- ### Task 3: apps/web-next — Next.js 15 app shell **Files:** - Modify: `apps/web-next/package.json` - Create: `apps/web-next/next.config.mjs` - Create: `apps/web-next/src/app/layout.tsx` - Create: `apps/web-next/src/app/page.tsx` - Create: `apps/web-next/src/app/api/trpc/[trpc]/route.ts` - Modify: `apps/web-next/tsconfig.json` - [ ] **Step 1: Update apps/web-next/package.json** ```json { "name": "@repo/web-next", "private": true, "version": "0.0.0", "type": "module", "scripts": { "build": "next build", "dev": "next dev --port 3000", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/api": "workspace:*", "@repo/api-client": "workspace:*", "@repo/ui": "workspace:*", "next": "^15.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0" } } ``` - [ ] **Step 2: Create next.config.mjs** ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ["@repo/api", "@repo/api-client", "@repo/core", "@repo/ui"], }; export default nextConfig; ``` - [ ] **Step 3: Create src/app/api/trpc/[trpc]/route.ts** ```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 }; ``` - [ ] **Step 4: Create src/app/layout.tsx** ```tsx import type { Metadata } from "next"; import { Providers } from "./providers"; export const metadata: Metadata = { title: "Template — Next.js", description: "Clean Architecture Monorepo Template", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` - [ ] **Step 5: Create src/app/providers.tsx** ```tsx "use client"; import { ApiProvider } from "@repo/api-client"; export function Providers({ children }: { children: React.ReactNode }) { return {children}; } ``` - [ ] **Step 6: Create src/app/page.tsx** ```tsx export default function Home() { return (

Template — Next.js

Clean Architecture Monorepo Template

); } ``` - [ ] **Step 7: Commit** ```bash git add apps/web-next/ pnpm-lock.yaml git commit -m "feat(web-next): add Next.js 15 app shell with tRPC endpoint" ``` --- ### Task 4: apps/web-tanstack — TanStack Start app shell **Files:** - Modify: `apps/web-tanstack/package.json` - Create: `apps/web-tanstack/vite.config.ts` - Create: `apps/web-tanstack/src/router.tsx` - Create: `apps/web-tanstack/src/routes/__root.tsx` - Create: `apps/web-tanstack/src/routes/index.tsx` - Modify: `apps/web-tanstack/tsconfig.json` - [ ] **Step 1: Update apps/web-tanstack/package.json** ```json { "name": "@repo/web-tanstack", "private": true, "version": "0.0.0", "type": "module", "scripts": { "build": "vite build", "dev": "vite dev --port 3002", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/api": "workspace:*", "@repo/api-client": "workspace:*", "@repo/ui": "workspace:*", "@tanstack/react-router": "^1.120.0", "@tanstack/react-start": "^1.120.0", "react": "^19.0.0", "react-dom": "^19.0.0", "vite": "^6.3.0" }, "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.4.0" } } ``` - [ ] **Step 2: Create vite.config.ts** ```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], }); ``` - [ ] **Step 3: Create src/routes/__root.tsx** ```tsx import { Outlet, createRootRoute } from "@tanstack/react-router"; import { ApiProvider } from "@repo/api-client"; export const Route = createRootRoute({ component: () => ( ), }); ``` - [ ] **Step 4: Create src/routes/index.tsx** ```tsx import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home, }); function Home() { return (

Template — TanStack Start

Clean Architecture Monorepo Template

); } ``` - [ ] **Step 5: Commit** ```bash git add apps/web-tanstack/ pnpm-lock.yaml git commit -m "feat(web-tanstack): add TanStack Start app shell with tRPC client" ``` --- ### Task 5: Install all dependencies and verify - [ ] **Step 1: Run pnpm install** Run: `pnpm install` Expected: All dependencies resolve. - [ ] **Step 2: Run turbo build** Run: `pnpm build` Expected: All packages build (apps may fail on `next build` / `vite build` without full setup — change to placeholder if needed). - [ ] **Step 3: Run core tests** Run: `cd packages/core && pnpm vitest run` Expected: All 22 tests pass. - [ ] **Step 4: Commit any remaining fixes** ```bash git add -A git commit -m "chore: finalize Plan 4 — API layer + app shells" ```