GameCraftGameCraft

Routing

Learn how to create and manage routes in ProductReady using Next.js App Router, tRPC, Hono OpenAPI, and Wouter for SPA routing.

Overview

ProductReady uses a hybrid routing architecture that combines Next.js App Router for server-side rendering, tRPC for type-safe APIs, Hono + OpenAPI for REST APIs, and Wouter for client-side SPA routing in dashboard areas.

This guide will help you understand when to use each routing approach and how to create new routes.


Main Application Routes

ProductReady comes with several pre-built routes ready to use:

Landing Page (/)

Route: src/app/[lang]/(home)/page.tsx

The marketing homepage featuring:

  • Hero section with call-to-action
  • Feature highlights
  • Pricing information
  • Responsive design with i18n support

Access: Public - no authentication required

Use case: Marketing, lead generation, product information


Dashboard (/dashboard)

Route: src/app/(dashboard)/dashboard/page.tsx

Main application interface with:

  • Task management system
  • Analytics and metrics
  • Recent activity feed
  • Quick actions and shortcuts
  • Uses Wouter for client-side SPA routing

Access: Protected - requires authentication (or ?demo=1 for demo mode)

Use case: Main user workspace, task tracking, data visualization


Agent Tasks (/agent)

Route: src/app/(dashboard)/agent/[[...slug]]/page.tsx

AI agent task management interface featuring:

  • Task creation and monitoring
  • Real-time streaming chat with AI agents
  • Background task execution with resume streams
  • Artifact generation and display (two-column layout)
  • Task status tracking and history

Access: Protected - requires authentication (or ?demo=1 for demo mode)

Use case: AI-powered automation, long-running tasks, agent workflows

Key Features:

  • Resume Streams: Tasks continue running even if user disconnects
  • Polymorphic Artifacts: Generated outputs can link to posts and other entities
  • Background Execution: Tasks run server-side without user presence

System Admin (/systemadmin)

Route: src/app/(dashboard)/systemadmin/[[...slug]]/page.tsx

System administration panel with role-based access control:

  • User management (create, edit, delete users)
  • System admin management (owner/admin roles)
  • System settings and configuration
  • Access logs and monitoring
  • Uses Wouter for client-side SPA routing

Access: Restricted - requires system admin privileges (owner or admin role)

Authorization:

  • Page-level: Checks system_admins table on every page load
  • API-level: All operations protected with systemAdminProcedure
  • Role hierarchy:
    • Owner: Full control, can manage other admins
    • Admin: Can manage users but not other admins

Use case: Platform administration, user provisioning, security management

Default Access: No users have admin access by default. Run pnpm db:seed to create owner account (admin@productready.dev). Password is randomly generated and shown in seed output, or set via SYSTEM_ADMIN_PASSWORD env var.


Routing Types

1. Next.js App Router (Server Pages)

Use for: SEO-critical pages, marketing pages, blog posts, documentation

Location: src/app/[lang]/(group)/page.tsx

Example:

// src/app/[lang]/(home)/pricing/page.tsx
export default function PricingPage() {
  return (
    <div>
      <h1>Pricing</h1>
      {/* SSR content with SEO metadata */}
    </div>
  );
}

// Optional: Add metadata for SEO
export const metadata = {
  title: "Pricing - ProductReady",
  description: "Choose the perfect plan for your needs",
};

Creating a new page:

  1. Create a file in the appropriate route group:

    • (home) - Public marketing pages
    • (legal) - Terms, privacy policy
    • (dashboard) - Dashboard root pages (with Wouter inside)
  2. Export a default React component

  3. Add metadata for SEO (optional)


2. tRPC API Routes (Type-Safe APIs)

Use for: Internal APIs, real-time data fetching, mutations, type-safe client-server communication

Location: src/server/routers/*.ts

Example:

Backend (Router):

// src/server/routers/posts.ts
import { z } from "zod";
import { protectedProcedure, publicProcedure, router } from "../trpc";

export const postsRouter = router({
  // Query - fetch data
  list: publicProcedure
    .input(
      z.object({
        limit: z.number().min(1).max(100).default(10),
        offset: z.number().min(0).default(0),
      })
    )
    .query(async ({ ctx, input }) => {
      const posts = await ctx.db.query.posts.findMany({
        limit: input.limit,
        offset: input.offset,
      });
      return { posts, total: posts.length };
    }),

  // Mutation - modify data
  create: protectedProcedure
    .input(
      z.object({
        title: z.string().min(1),
        content: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const post = await ctx.db.insert(posts).values({
        userId: ctx.user.id,
        title: input.title,
        content: input.content,
      });
      return post;
    }),
});

Register the router:

// src/server/routers/index.ts
import { router } from "../trpc";
import { postsRouter } from "./posts";

export const appRouter = router({
  posts: postsRouter,
  // ... other routers
});

Frontend (Client):

"use client";
import { api } from "~/lib/trpc/client";

function PostsList() {
  const { data, isLoading } = api.posts.list.useQuery({ limit: 10, offset: 0 });
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      {data?.posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

Benefits:

  • ✅ Full TypeScript end-to-end type safety
  • ✅ No need to write API endpoints manually
  • ✅ Built-in React Query integration
  • ✅ Automatic request deduplication & caching

3. Hono OpenAPI Routes (REST APIs)

Use for: Public REST APIs, third-party integrations, mobile apps, webhooks

Location: src/app/api/v1/[[...server]]/app.ts

Example:

// src/app/api/v1/[[...server]]/app.ts
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";

export const app = new OpenAPIHono().basePath("/api/v1");

// Define route with OpenAPI spec
const getUserRoute = createRoute({
  method: "get",
  path: "/users/{id}",
  request: {
    params: z.object({
      id: z.string().openapi({ example: "123" }),
    }),
  },
  responses: {
    200: {
      description: "User details",
      content: {
        "application/json": {
          schema: z.object({
            id: z.string(),
            name: z.string(),
            email: z.string().email(),
          }),
        },
      },
    },
    404: {
      description: "User not found",
      content: {
        "application/json": {
          schema: z.object({
            error: z.string(),
          }),
        },
      },
    },
  },
});

// Implement the handler
app.openapi(getUserRoute, async (c) => {
  const { id } = c.req.param();
  
  const user = await db.query.users.findFirst({
    where: eq(users.id, id),
  });
  
  if (!user) {
    return c.json({ error: "User not found" }, 404);
  }
  
  return c.json({
    id: user.id,
    name: user.name,
    email: user.email,
  });
});

// Auto-generated OpenAPI docs at /api/v1/openapi.json
app.doc("/openapi.json", {
  openapi: "3.0.0",
  info: {
    title: "ProductReady API",
    version: "1.0.0",
  },
});

Access:

  • API endpoint: https://yourapp.com/api/v1/users/123
  • OpenAPI spec: https://yourapp.com/api/v1/openapi.json
  • Swagger UI: https://yourapp.com/api/v1/doc

Benefits:

  • ✅ Automatic OpenAPI documentation generation
  • ✅ Interactive Swagger UI for testing
  • ✅ Zod schema validation
  • ✅ Perfect for external/public APIs

System Admin API:

A special subset of OpenAPI routes for site-level operations using a global secret key (SYSTEM_ADMIN_API_SECRET_KEY).

# System admin endpoint example
GET /api/v1/system-admin/users
Authorization: Bearer $SYSTEM_ADMIN_API_SECRET_KEY

Use for: Microservices communication, cross-service user management, system-wide admin operations

Learn more: API Routing Decision Guide


4. Wouter SPA Routes (Client-Side Routing)

Use for: Dashboard internal navigation, wizard flows, tabs, multi-step forms

Location: src/app/(dashboard)/*/[[...slug]]/page.tsx

Example:

// src/app/(dashboard)/agent/[[...slug]]/page.tsx
"use client";

export const dynamic = "force-dynamic"; // Required for Wouter

import { Route, Router, Switch } from "wouter";
import { DashLink } from "~/components/DashLink";

export default function AgentPage() {
  return (
    <SPAWrapper>
      <Router base="/agent">
        <Switch>
          {/* List view */}
          <Route path="/">
            <AgentList />
          </Route>
          
          {/* Detail view with ID */}
          <Route path="/:id">
            {(params) => <AgentDetail taskId={params.id} />}
          </Route>
          
          {/* Settings */}
          <Route path="/settings">
            <AgentSettings />
          </Route>
          
          {/* 404 fallback */}
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Router>
    </SPAWrapper>
  );
}

Navigation with DashLink:

import { DashLink } from "~/components/DashLink";

function TaskItem({ task }) {
  return (
    <DashLink href={`/${task.id}`}>
      <div>{task.title}</div>
    </DashLink>
  );
}

Benefits:

  • ✅ Instant client-side navigation (no page reload)
  • ✅ Preserves demo mode (?demo=1) automatically
  • ✅ Better UX for dashboard interactions
  • ✅ Smaller bundle size than Next.js router in client components

When to use DashLink:

  • ✅ Navigation within SPA routes (/agent/*, /dashboard/*, /kadmin/*)
  • ❌ Don't use for cross-section navigation (use Next.js <Link> instead)

Route Examples

Example 1: Create a New Public Page

Goal: Add a new "Features" page at /features

// src/app/[lang]/(home)/features/page.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "Features - ProductReady",
  description: "Explore powerful features of ProductReady",
};

export default function FeaturesPage() {
  return (
    <div className="container mx-auto py-12">
      <h1 className="text-4xl font-bold">Features</h1>
      <p>Discover what makes ProductReady special...</p>
    </div>
  );
}

Access: https://yourapp.com/en/features or https://yourapp.com/zh-CN/features


Example 2: Add a New tRPC Endpoint

Goal: Add an endpoint to fetch user statistics

// src/server/routers/analytics.ts
import { z } from "zod";
import { protectedProcedure, router } from "../trpc";

export const analyticsRouter = router({
  getUserStats: protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Fetch stats from database
      const stats = await ctx.db.query.userStats.findFirst({
        where: eq(userStats.userId, input.userId),
      });
      
      return {
        totalPosts: stats?.totalPosts ?? 0,
        totalViews: stats?.totalViews ?? 0,
        lastActive: stats?.lastActive ?? new Date(),
      };
    }),
});

// Register in src/server/routers/index.ts
export const appRouter = router({
  analytics: analyticsRouter,
  // ...
});

Usage:

"use client";
const { data } = api.analytics.getUserStats.useQuery({ userId: user.id });

Example 3: Add a Wouter SPA Route

Goal: Add a "History" tab in the agent dashboard

// src/app/(dashboard)/agent/[[...slug]]/page.tsx
<Router base="/agent">
  <Switch>
    <Route path="/history">
      <AgentHistory />
    </Route>
    {/* ... other routes */}
  </Switch>
</Router>

// Navigation
<DashLink href="/history">
  <Button>View History</Button>
</DashLink>

Decision Tree: Which Routing to Use?

┌─────────────────────────────────────────┐
│   Need SEO / Server-Side Rendering?    │
└──────────────┬──────────────────────────┘

       ┌───────┴────────┐
       │ YES            │ NO
       ▼                ▼
┌─────────────┐  ┌─────────────────────┐
│  Next.js    │  │  Internal/External? │
│  App Router │  └──────────┬──────────┘
└─────────────┘             │
  - Marketing       ┌────────┴─────────┐
  - Blog            │ Internal         │ External
  - Docs            ▼                  ▼
  - Legal     ┌──────────────┐  ┌─────────────┐
              │ Type Safety? │  │ Hono OpenAPI│
              └──────┬───────┘  └─────────────┘
                     │            - REST API
           ┌─────────┴────────┐   - Webhooks
           │ YES              │ NO│ Mobile apps
           ▼                  ▼
      ┌─────────┐      ┌──────────┐
      │  tRPC   │      │  Wouter  │
      └─────────┘      └──────────┘
        - Queries       - Dashboard
        - Mutations     - SPA routing
        - Subscriptions - Client nav

Best Practices

✅ Do's

  • Use tRPC for all internal API calls (queries, mutations)
  • Use Wouter for dashboard navigation to improve UX
  • Use DashLink within SPA routes to preserve demo mode
  • Add metadata to all public Next.js pages for SEO
  • Use route groups (group) to organize pages logically
  • Validate inputs with Zod schemas in both tRPC and Hono routes
  • Use protectedProcedure for authenticated endpoints in tRPC
  • Document OpenAPI routes with descriptions and examples

❌ Don'ts

  • Don't use Wouter for cross-section navigation (use Next.js <Link>)
  • Don't mix routing types in the same component (pick one)
  • Don't forget to add export const dynamic = "force-dynamic" for Wouter pages
  • Don't expose sensitive data in OpenAPI routes without authentication
  • Don't skip input validation in API routes
  • Don't hardcode URLs - use environment variables for external services

File Structure

src/
├── app/
│   ├── [lang]/              # Internationalized routes
│   │   ├── (home)/          # Public pages
│   │   │   ├── page.tsx     # Homepage (/)
│   │   │   ├── pricing/     # Pricing page
│   │   │   └── features/    # Features page
│   │   ├── (legal)/         # Legal pages
│   │   │   ├── privacy-policy/
│   │   │   └── terms-of-service/
│   │   ├── docs/            # Documentation
│   │   └── blog/            # Blog posts
│   ├── (dashboard)/         # Dashboard with Wouter
│   │   ├── agent/           # Agent SPA routes
│   │   │   └── [[...slug]]/page.tsx
│   │   ├── dashboard/       # Dashboard SPA routes
│   │   └── kadmin/          # Admin SPA routes
│   └── api/                 # API routes
│       ├── trpc/            # tRPC endpoint
│       ├── v1/              # Hono OpenAPI REST API
│       ├── auth/            # Better Auth
│       └── agent/           # Custom API routes
├── server/
│   └── routers/             # tRPC routers
│       ├── index.ts         # Root router
│       ├── posts.ts
│       ├── users.ts
│       └── agent-tasks.ts
└── components/
    └── DashLink.tsx         # Wouter link with demo mode

Environment Variables

Required for API routes:

# Database
DATABASE_URL="postgresql://..."

# Authentication (Better Auth)
BETTER_AUTH_SECRET="your-secret-key"
BETTER_AUTH_URL="http://localhost:3000"

# OpenAI (for AI features)
OPENAI_API_KEY="sk-..."

# Optional: External services for Hono APIs
STRIPE_SECRET_KEY="sk_test_..."

Troubleshooting

Issue: Wouter routes not working

Solution: Make sure you added export const dynamic = "force-dynamic" at the top of your page component.

"use client";
export const dynamic = "force-dynamic"; // ← Add this

import { Router } from "wouter";
// ...

Issue: tRPC type errors

Solution: Regenerate types after adding new routers:

pnpm typecheck

Make sure your router is registered in src/server/routers/index.ts.


Issue: Demo mode not persisting in SPA

Solution: Use DashLink instead of regular <Link> or <a> tags:

// ❌ Wrong
<Link href="/agent/123">View Task</Link>

// ✅ Correct
<DashLink href="/123">View Task</DashLink>

Issue: OpenAPI route returns 404

Solution: Check that you're accessing the correct path with /api/v1 prefix:

# ❌ Wrong
curl https://yourapp.com/users/123

# ✅ Correct
curl https://yourapp.com/api/v1/users/123

Next Steps


Additional Resources

On this page