15 KiB
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
{
"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
{
"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
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
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
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
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
export { appRouter, type AppRouter } from "./router/index.js";
- Step 8: Run pnpm install and commit
Run: pnpm install
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
{
"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
{
"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
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
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
"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
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
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
{
"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
/** @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
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
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
"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
export default function Home() {
return (
<main>
<h1>Template — Next.js</h1>
<p>Clean Architecture Monorepo Template</p>
</main>
);
}
- Step 7: Commit
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
{
"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
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
- Step 3: Create src/routes/__root.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
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
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
git add -A
git commit -m "chore: finalize Plan 4 — API layer + app shells"