Add Plan 4: API Layer + App Shells implementation plan

This commit is contained in:
2026-04-06 14:48:00 +02:00
parent f313d6841f
commit 1b93fecb7f

View File

@@ -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<AppRouter>();
```
- [ ] **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<AppRouter>({
links: [httpBatchLink({ url: trpcUrl })],
});
return (
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</TRPCProvider>
);
}
```
- [ ] **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 (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
```
- [ ] **Step 5: Create src/app/providers.tsx**
```tsx
"use client";
import { ApiProvider } from "@repo/api-client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
}
```
- [ ] **Step 6: Create src/app/page.tsx**
```tsx
export default function Home() {
return (
<main>
<h1>Template Next.js</h1>
<p>Clean Architecture Monorepo Template</p>
</main>
);
}
```
- [ ] **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: () => (
<ApiProvider trpcUrl="http://localhost:3000/api/trpc">
<Outlet />
</ApiProvider>
),
});
```
- [ ] **Step 4: Create src/routes/index.tsx**
```tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: Home,
});
function Home() {
return (
<main>
<h1>Template TanStack Start</h1>
<p>Clean Architecture Monorepo Template</p>
</main>
);
}
```
- [ ] **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"
```