Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Declarative Routing

Type-safe route definitions

Overview

Declarative routing provides type-safe navigation by generating route functions from page.info.ts files. Each route has full TypeScript support for parameters and search params.

page.info.ts Files

Every page can have a page.info.ts file that defines its route configuration.

Basic Route

// src/app/[locale]/(site)/about/page.info.ts
import { z } from "zod";

export const Route = {
  name: "PageAbout" as const,
  params: z.object({}),
};

The name property must use as const to ensure proper type inference.

Route with Parameters

// src/app/[locale]/(site)/users/[id]/page.info.ts
import { z } from "zod";

export const Route = {
  name: "PageUserId" as const,
  params: z.object({
    id: z.string().uuid(),
  }),
};

Route with Search Params

// src/app/[locale]/(site)/search/page.info.ts
import { localeSchema } from "@/i18n/routing";
import { siteSearchParams } from "@/lib/utils/schema-utils";
import { z } from "zod";

export const Route = {
  name: "PageSearch" as const,
  params: localeSchema.extend({}),
  search: siteSearchParams.extend({
    q: z.string().optional(),
    page: z.coerce.number().optional(),
  }),
};

Important Rules:

  • Always extend localeSchema for params
  • Always extend siteSearchParams for search params
  • Search params must be optional (.optional())
  • Search params must be z.string() - use z.coerce for type transformation

Route Definition

Required Properties

PropertyTypeDescription
namestringRoute identifier (as const)
paramsZodObjectZod schema for params

Optional Properties

PropertyTypeDescription
searchZodObjectZod schema for search params

Generated Route Functions

Each page.info.ts generates route functions with TypeScript support:

Route Function

import { PageUserId } from "@/routes";

// Type-safe URL generation
const url = PageUserId({ id: "123" });
// Returns: "/en/users/123" (or current locale)

Link Component

// Basic link
<PageUserId.Link id="123">View User</PageUserId.Link>

// With additional props
<PageUserId.Link id="123" className="text-blue-500">
  View User
</PageUserId.Link>

ParamsLink Component

For passing all params in a single object:

<PageUserId.ParamsLink params={{ id: "123" }}>
  View User
</PageUserId.ParamsLink>

Route Name

const routeName = PageUserId.routeName; // "PageUserId"

Route Parameters

Path Parameters

Defined in Next.js folder structure:

src/app/[locale]/(site)/
├── users/
│   └── [id]/
│       ├── page.tsx
│       └── page.info.ts  // Defines id param
export const Route = {
  name: "PageUserId" as const,
  params: z.object({
    id: z.string(),
  }),
};

Usage:

<PageUserId.Link id={user.id}>View</PageUserId.Link>

Multiple Parameters

// Route: /posts/[category]/[slug]/page.info.ts
export const Route = {
  name: "PagePostCategorySlug" as const,
  params: z.object({
    category: z.string(),
    slug: z.string(),
  }),
};

Usage:

<PagePostCategorySlug.Link category="tech" slug="my-post">
  Read Post
</PagePostCategorySlug.Link>

Locale Parameter

Always extend localeSchema:

import { localeSchema } from "@/i18n/routing";

export const Route = {
  name: "PageMyPage" as const,
  params: localeSchema.extend({
    id: z.string(),
  }),
};

Search Parameters

Defining Search Params

import { siteSearchParams } from "@/lib/utils/schema-utils";

export const Route = {
  name: "PageSearch" as const,
  params: localeSchema.extend({}),
  search: siteSearchParams.extend({
    q: z.string().optional(),
    page: z.coerce.number().optional(),
    sort: z.enum(["asc", "desc"]).optional(),
  }),
};

Usage

// Link with search params
<PageSearch.Link search={{ q: "hello", page: 1 }}>
  Search
</PageSearch.Link>

// URL with search params
const url = PageSearch({}, { q: "hello", page: 1 });
// Returns: "/en/search?q=hello&page=1"

Type Coercion

Search params from URL are always strings. Use z.coerce for type conversion:

search: siteSearchParams.extend({
  page: z.coerce.number().optional(),      // "1" → 1
  active: z.coerce.boolean().optional(),   // "true" → true
})

Accessing Route Params

In Page Components

// src/app/[locale]/(site)/users/[id]/page.tsx
type Props = { params: Promise<{ locale: string; id: string }> };

export default async function Page({ params }: Props) {
  const { id } = await params;
  return <div>User ID: {id}</div>;
}

In Client Components

"use client";

import { useParams } from "next/navigation";

export function UserProfile() {
  const params = useParams<{ id: string }>();
  return <div>User ID: {params.id}</div>;
}

Extract Params from URL

import { extractParamsFromRoute } from "@/routes/makeRoute";
import { usePathname } from "next/navigation";

const pathname = usePathname();
const params = extractParamsFromRoute(pathname, PageUserId);
// params.id is available if route matches

Route Groups

Routes inside parentheses () are not included in the URL:

src/app/[locale]/
├── (site)/           # Not in URL
│   ├── about/
│   └── pricing/
├── (auth)/           # Not in URL
│   ├── login/
│   └── register/
└── (dashboard)/      # Not in URL
    └── dashboard/

Naming Conventions

Route Name Pattern

Route names follow this pattern:

Page + [Folder1] + [Folder2] + ... + [FolderN]

Examples:

Route PathName
/aboutPageAbout
/dashboardPageDashboard
/dashboard/api-keysPageDashboardApiKeys
/dashboard/organizations/[slug]PageDashboardOrganizationSlug
/settings/profilePageSettingsProfile

Route groups like (site) and (dashboard) are excluded from the name.

Best Practices

1. Always Use Type-Safe Routes

// ✅ Good - Type-safe
<PageUserId.Link id={user.id}>View</PageUserId.Link>

// ❌ Bad - String literal
<Link href={`/users/${user.id}`}>View</Link>

2. Extend Required Schemas

// ✅ Good
params: localeSchema.extend({ id: z.string() }),
search: siteSearchParams.extend({ q: z.string().optional() }),

// ❌ Bad - Missing extensions
params: z.object({ id: z.string() }),
search: z.object({ q: z.string().optional() }),

3. Make Search Params Optional

// ✅ Good
search: siteSearchParams.extend({
  q: z.string().optional(),
}),

// ❌ Bad - Non-optional causes validation errors
search: siteSearchParams.extend({
  q: z.string(),
}),

4. Use Descriptive Names

// ✅ Good
name: "PageDashboardApiKeys" as const,

// ❌ Bad - Unclear
name: "PageDak" as const,

Next Steps

Navigation

New Page Template

i18n Setup

On this page

Overview
page.info.ts Files
Basic Route
Route with Parameters
Route with Search Params
Route Definition
Required Properties
Optional Properties
Generated Route Functions
Route Function
Link Component
ParamsLink Component
Route Name
Route Parameters
Path Parameters
Multiple Parameters
Locale Parameter
Search Parameters
Defining Search Params
Usage
Type Coercion
Accessing Route Params
In Page Components
In Client Components
Extract Params from URL
Route Groups
Naming Conventions
Route Name Pattern
Best Practices
1. Always Use Type-Safe Routes
2. Extend Required Schemas
3. Make Search Params Optional
4. Use Descriptive Names
Next Steps