Add Plan 4: API Layer + App Shells implementation plan
This commit is contained in:
628
docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md
Normal file
628
docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md
Normal 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"
|
||||
```
|
||||
Reference in New Issue
Block a user