629 lines
15 KiB
Markdown
629 lines
15 KiB
Markdown
# 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"
|
|
```
|