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

Routers

Organizing tRPC routers

Creating a Router

Routers organize related procedures into feature-based modules.

Basic Router Structure

// src/trpc/procedures/routers/my-resource.ts
import { createTRPCRouter } from "@/trpc/init";
import { authProcedure } from "@/trpc/procedures/trpc";
import { z } from "zod";

export const myResourceRouter = createTRPCRouter({
  list: authProcedure
    .meta({ rateLimit: "QUERY" })
    .query(async ({ ctx }) => {
      return {
        success: true,
        payload: await ctx.db.myResource.list(),
      };
    }),

  get: authProcedure
    .meta({ rateLimit: "QUERY" })
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      return {
        success: true,
        payload: await ctx.db.myResource.get({ id: input.id }),
      };
    }),

  create: authProcedure
    .meta({ rateLimit: "MUTATION" })
    .input(myResourceSchema)
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.myResource.create(input);
      return {
        success: true,
        message: ctx.t("toasts.saved"),
        payload: result,
      };
    }),
});

Combining Routers

Routers are combined in the root router to create the full API surface.

Root Router

// src/trpc/procedures/root.ts
import { createTRPCRouter } from "@/trpc/init";
import { myResourceRouter } from "./routers/my-resource";
import { anotherRouter } from "./routers/another-resource";

export const appRouter = createTRPCRouter({
  myResource: myResourceRouter,
  another: anotherRouter,
});

export type AppRouter = typeof appRouter;

Usage

// Client-side
const { data } = api.myResource.list.useQuery();
const { data } = api.another.get.useQuery({ id: "123" });

// Server-side
const data = await api.myResource.list();

Router Organization

Feature-Based Structure

Organize routers by feature or domain:

src/trpc/procedures/routers/
├── auth.ts           # Authentication operations
├── users.ts          # User management
├── organizations.ts  # Organization operations
├── api-keys.ts       # API key management
└── subscriptions.ts  # Subscription management

Nested Routers

For complex features, create nested routers:

export const organizationRouter = createTRPCRouter({
  // Basic operations
  list: authProcedure.query(/* ... */),
  get: authProcedure.input(/* ... */).query(/* ... */),

  // Nested router for members
  members: createTRPCRouter({
    list: authProcedure.query(/* ... */),
    invite: authProcedure.mutation(/* ... */),
    remove: authProcedure.mutation(/* ... */),
  }),
});

Usage:

const { data } = api.organization.list.useQuery();
const { data } = api.organization.members.list.useQuery();

Best Practices

1. Consistent Response Format

Always return ActionResponse:

return {
  success: true,
  message: ctx.t("toasts.saved"),
  payload: data,
} satisfies ActionResponse;

2. Input Validation

Use Zod schemas for input validation:

import { z } from "zod";

const createSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
});

create: authProcedure
  .input(createSchema)
  .mutation(async ({ input }) => {
    // input is fully typed and validated
  })

3. Rate Limiting

Add rate limiting to all procedures:

list: authProcedure
  .meta({ rateLimit: "QUERY" })
  .query(/* ... */),

create: authProcedure
  .meta({ rateLimit: "MUTATION" })
  .mutation(/* ... */),

4. Context Usage

Leverage context for common operations:

export const myRouter = createTRPCRouter({
  list: authProcedure.query(async ({ ctx }) => {
    // ctx.user - Current user (available in authProcedure)
    // ctx.db - Database operations
    // ctx.t - Translations
    // ctx.headers - Request headers
    // ctx.session - Current session

    return await ctx.db.myResource.list({
      where: { userId: ctx.user.id },
    });
  }),
});

Template

For step-by-step instructions on creating a new tRPC router, see the New tRPC Router Template.

Next Steps

Procedures

OpenAPI

New Router Template

On this page

Creating a Router
Basic Router Structure
Combining Routers
Root Router
Usage
Router Organization
Feature-Based Structure
Nested Routers
Best Practices
1. Consistent Response Format
2. Input Validation
3. Rate Limiting
4. Context Usage
Template
Next Steps