diff --git a/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md b/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md new file mode 100644 index 0000000..46fe84f --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md @@ -0,0 +1,628 @@ +# 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" +```