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

tRPC Middleware

Understanding the middleware chain and request processing pipeline

Overview

Every tRPC request passes through a series of middleware functions that handle cross-cutting concerns like bot detection, internationalization, rate limiting, timing, and error handling. These middleware functions execute in a specific order to ensure consistent behavior across all API endpoints.

Middleware Execution Order

The middleware chain processes requests in this order:

  1. Bot Detection - Blocks identified bots on protected routes
  2. i18n - Sets up localized Zod error messages
  3. Rate Limiting - Applies request limits based on configuration
  4. Timing - Logs procedure execution time
  5. Error Catcher - Translates and formats errors
export const publicProcedure = trpc.procedure
  .use(botDetectionMiddleware)
  .use(i18nMiddleware)
  .use(rateLimitMiddleware)
  .use(timingMiddleware)
  .use(errorCatcherMiddleware);

Each middleware can:

  • Inspect the request context
  • Modify the context for subsequent middleware
  • Short-circuit the request (e.g., return error for bots)
  • Execute logic before or after the next middleware

1. Bot Detection Middleware

Identifies and blocks automated bots from accessing protected endpoints.

Purpose

  • Prevent automated scraping
  • Reduce spam and abuse
  • Protect sensitive endpoints
  • Allow verified bots (like search engines)

Implementation

const botDetectionMiddleware = trpc
  .middleware(async ({ next, meta, ctx, input, path }) => {
    // Block bots if configured in meta
    if (meta?.blockBots && await getFeatureFlag("disableBotIdCheck") !== true) {
      const verification = await checkBotId();

      if (verification.isBot) {
        console.warn("trpc", "Bot detected: is good bot?", verification.isVerifiedBot);
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "Access denied: Bot detected",
        });
      }
    }

    return next({ ctx, input });
  });

Usage

Enable bot detection per-procedure using meta:

export const sensitiveRouter = createTRPCRouter({
  getData: publicProcedure
    .meta({ blockBots: true })
    .query(async () => {
      // This endpoint blocks bots
      return { data: "sensitive" };
    }),
});

Configuration

  • Global toggle - Use disableBotIdCheck feature flag to disable
  • Per-procedure - Set blockBots: true in meta to enable
  • Verified bots - Search engine crawlers are allowed through

2. i18n Middleware

Sets up internationalization context for the request, including localized error messages.

Purpose

  • Provide localized Zod validation errors
  • Set error message language based on user locale
  • Ensure consistent i18n across server procedures

Implementation

const i18nMiddleware = trpc
  .middleware(async ({ next, ctx, input }) => {
    // Set Zod error map for server-side validation errors
    locaZodErrorMap(ctx.locale);

    return next({ ctx, input });
  });

How It Works

  1. Extracts locale from context (already set during tRPC context creation)
  2. Applies locale-specific error messages to Zod validation
  3. All subsequent Zod schema validations use the correct language

Example

// Schema validation
const userSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18),
});

// If user's locale is "es" (Spanish), errors are in Spanish:
// "El email no es válido"
// If locale is "en" (English):
// "Invalid email address"

The error messages are automatically localized based on the user's preferred language.

3. Rate Limiting Middleware

Prevents abuse by limiting the number of requests per user/IP address.

Purpose

  • Prevent API abuse
  • Protect against DDoS attacks
  • Ensure fair resource usage
  • Different limits for different operation types

Implementation

const rateLimitMiddleware = trpc
  .middleware(async ({ next, meta, ctx, path, type }) => {
    // Skip rate limiting if disabled globally
    if (!Config.RateLimit.ENABLED) {
      return next({ ctx });
    }

    // Skip rate limiting if disabled in meta
    if (meta?.rateLimit === false) {
      return next({ ctx });
    }

    // Get rate limit config
    const rateLimitKey = meta?.rateLimit || (type === "mutation" ? "MUTATION" : "QUERY");
    const config = RATE_LIMIT_CONFIGS[rateLimitKey];

    if (!config) {
      console.warn(`trpc: Rate limit config not found for key: ${rateLimitKey}`);
      return next({ ctx });
    }

    // Extract IP address from request
    const ip = getIpAddress(ctx.request);

    // Generate rate limit key
    const key = generateRateLimitKey({
      ip,
      userId: ctx.user?.id,
      path,
    });

    // Apply rate limiting
    const secondsBeforeNext = await applyRateLimit(key, config, {
      configKey: rateLimitKey,
      ip,
      userId: ctx.user?.id,
      path,
    });

    if (secondsBeforeNext && secondsBeforeNext > 0) {
      throw new TRPCError({
        code: "TOO_MANY_REQUESTS",
        message: ctx.t("trpcErrors.TOO_MANY_REQUESTS", { seconds: secondsBeforeNext }),
      });
    }

    return next({ ctx });
  });

Rate Limit Configurations

Defined in src/lib/rate-limit/rate-limiter.ts:

TypeRequestsWindowUse Case
QUERY1001 minuteRead operations
MUTATION301 minuteWrite operations
AUTH105 minutesLogin/signup
PAYMENT51 minutePayment operations

Usage

Specify rate limit type in procedure meta:

export const userRouter = createTRPCRouter({
  // Use default QUERY rate limit (100/min)
  list: publicProcedure
    .query(async () => {
      return await userOperations.list();
    }),

  // Use MUTATION rate limit (30/min)
  create: publicProcedure
    .meta({ rateLimit: "MUTATION" })
    .input(createUserSchema)
    .mutation(async ({ input }) => {
      return await userOperations.create(input);
    }),

  // Disable rate limiting
  health: publicProcedure
    .meta({ rateLimit: false })
    .query(() => ({ status: "ok" })),
});

Rate Limit Key Strategy

The rate limit key combines:

  • IP address - For unauthenticated requests
  • User ID - For authenticated requests
  • Procedure path - Optional per-endpoint limiting
// Example keys:
// "rate_limit:ip:192.168.1.1:users.list"
// "rate_limit:user:abc123:organizations.create"

4. Timing Middleware

Measures and logs the execution time of each procedure.

Purpose

  • Monitor API performance
  • Identify slow endpoints
  • Debug performance issues
  • Track performance over time

Implementation

const timingMiddleware = trpc
  .middleware(async ({ next, path, ctx, input }) => {
    const start = Date.now();

    const result = await next({ ctx, input });

    const end = Date.now();
    console.log(`⏰ [TRPC] ${path} took ${end - start}ms`);

    return result;
  });

Output

Console logs show timing for each request:

⏰ [TRPC] users.list took 45ms
⏰ [TRPC] organizations.getById took 12ms
⏰ [TRPC] subscriptions.create took 234ms

Best Practices

  • Monitor for procedures taking > 1000ms
  • Investigate and optimize slow endpoints
  • Consider caching for frequently accessed data
  • Use database indexes for common queries

5. Error Catcher Middleware

Translates errors into user-friendly, localized messages.

Purpose

  • Provide consistent error format
  • Translate errors to user's language
  • Preserve Zod validation errors for forms
  • Convert technical errors to user-friendly messages

Implementation

const errorCatcherMiddleware = trpc
  .middleware(async ({ next, ctx, input }) => {
    const resp = await next({ ctx, input });

    // Customize error if tRPC captured it in the response
    if (!resp.ok) {
      // Let Zod validation errors propagate (for form handling)
      if (resp.error.code === "BAD_REQUEST" && resp.error.cause instanceof ZodError) {
        return resp;
      }

      const error = resp.error;

      // Get the underlying error (could be nested in TRPCError cause)
      const underlyingError = error instanceof TRPCError
        ? (error.cause ?? error)
        : error;

      // Translate the underlying error
      const errorTranslation = translateError(underlyingError, { 
        t: ctx.t, 
        captureScope: "trpc" 
      });

      // Return success with error message
      return {
        ok: true,
        data: {
          success: false,
          message: errorTranslation.message,
        } as any,
        marker: "middlewareMarker" as any,
      };
    }

    return resp;
  });

Error Flow

  1. Zod validation errors - Pass through unchanged (used by forms)
  2. Custom errors - Translated to user's language
  3. Unknown errors - Converted to generic message
  4. Sentry integration - Errors automatically logged

Custom Error Classes

Define custom errors in src/lib/errors/:

import { NotFoundError, ValidationError, UnauthorizedError } from "@/lib/errors";

// In a procedure
if (!user) {
  throw new NotFoundError("User");
  // Becomes: "User not found"
}

if (!user.emailVerified) {
  throw new ValidationError("Email must be verified");
}

if (user.id !== ctx.user?.id) {
  throw new UnauthorizedError();
  // Becomes: "Unauthorized access"
}

Error Response Format

{
  success: false,
  message: "User not found", // Localized message
  data: {
    zodError: null, // Only present for validation errors
  }
}

Maintenance Mode

A special middleware layer can block all mutations during maintenance:

.use(async ({ ctx, next, signal, meta, path, input, type }) => {
  // Check if flag to disable all submits is enabled
  if (
    meta?.type === "mutation" &&
    !ALWAYS_ALLOWED_PROCEDURES.includes(path) &&
    (await getFeatureFlag("disableSubmit")) === true
  ) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: ctx.t("miscellaneous.maintenanceMode.featureDisabled"),
    });
  }

  return next({ ctx, input });
})

Always-Allowed Procedures

Certain critical procedures bypass maintenance mode:

const ALWAYS_ALLOWED_PROCEDURES: string[] = [
  "auth.login",
  "auth.logout",
  "auth.refreshSession",
  "auth.verifyEmail",
  "auth.resendVerification",
  "featureFlags.upsert"
];

Creating Custom Middleware

You can create custom middleware for your specific needs:

const customMiddleware = trpc.middleware(async ({ next, ctx, meta }) => {
  // Before procedure execution
  console.log("Before:", meta);
  
  // Execute next middleware
  const result = await next({ ctx });
  
  // After procedure execution
  console.log("After:", result);
  
  return result;
});

// Use in a procedure
export const customProcedure = publicProcedure
  .use(customMiddleware)
  .query(async () => {
    return { data: "Hello" };
  });

Best Practices

  1. Keep middleware fast - Slow middleware affects all requests
  2. Use meta for configuration - Don't hardcode logic in middleware
  3. Log appropriately - Too much logging can impact performance
  4. Handle errors gracefully - Don't let middleware crash
  5. Test middleware - Unit test each middleware function
  6. Document behavior - Explain what each middleware does

Related Documentation

  • tRPC Guide - Setting up tRPC routers and procedures
  • Error Handling - Custom error classes and translation
  • Rate Limiting - Detailed rate limit configuration
  • i18n - Internationalization setup

Source Code

See the complete implementation in:

  • src/trpc/procedures/trpc.ts - Middleware definitions
  • src/lib/rate-limit/rate-limiter.ts - Rate limit configs
  • src/lib/errors/translate-error.ts - Error translation

On this page

Overview
Middleware Execution Order
1. Bot Detection Middleware
Purpose
Implementation
Usage
Configuration
2. i18n Middleware
Purpose
Implementation
How It Works
Example
3. Rate Limiting Middleware
Purpose
Implementation
Rate Limit Configurations
Usage
Rate Limit Key Strategy
4. Timing Middleware
Purpose
Implementation
Output
Best Practices
5. Error Catcher Middleware
Purpose
Implementation
Error Flow
Custom Error Classes
Error Response Format
Maintenance Mode
Always-Allowed Procedures
Creating Custom Middleware
Best Practices
Related Documentation
Source Code