Files
agentic-dev/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md

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"