Skip to content
GuidesDocs 1.3.x

Per-Method Interceptor Routing ​

By default, interceptors in the interceptors array apply to every request. In practice, you often need different interceptors for different services or methods -- authentication for admin endpoints, aggressive timeouts for fast reads, circuit breakers for external-facing APIs.

Connectum provides three approaches for per-method interceptor routing.

Approach 1: ConnectRPC Native Per-Service/Per-Method ​

Connectum's defineService() forwards its third argument to ConnectRPC's router.service() options. Use that argument to scope interceptors to a service:

typescript
import { defineService } from '@connectum/core';
import { GreeterService } from '#gen/greeter_pb.ts';

export const greeterService = defineService(GreeterService, greeterImpl, {
  interceptors: [requireAuth, auditLog],
});

Use this approach when interceptors belong to one service. For one method within that service, use the filtering helper below. Raw router callbacks are not ServiceDefinition values accepted by createServer({ services }).

Approach 2: createMethodFilterInterceptor ​

createMethodFilterInterceptor is a declarative helper for routing interceptors to methods based on wildcard patterns. It produces a single Interceptor you can add to the global chain:

typescript
import {
  createMethodFilterInterceptor,
  createTimeoutInterceptor,
  createCircuitBreakerInterceptor,
} from '@connectum/interceptors';

const perMethodInterceptor = createMethodFilterInterceptor({
  // Global wildcard: applies to all methods
  '*': [logRequest],

  // Service wildcard: applies to all methods of the service
  'admin.v1.AdminService/*': [requireAdmin],

  // Exact match: applies to a specific method only
  'user.v1.UserService/DeleteUser': [requireAdmin, auditLog],
});

const server = createServer({
  services: [routes],
  interceptors: [perMethodInterceptor],
});

Supported Patterns ​

PatternDescriptionExample
"*"All methods of all services"*": [logRequest]
"package.Service/*"All methods of a service"admin.v1.AdminService/*": [auth]
"package.Service/Method"Exact method match"user.v1.UserService/GetUser": [cache]

TIP

Pattern keys use the protobuf fully-qualified service name (service.typeName) plus the method name: "package.v1.ServiceName/MethodName".

Resolution Order ​

All matching patterns execute sequentially, from most general to most specific:

If no patterns match, the request passes through to the next interceptor in the chain unchanged.

Invalid Patterns ​

The following patterns throw an error at creation time:

typescript
// Empty service name -- throws Error
createMethodFilterInterceptor({ '/*': [auth] });

// No slash separator -- throws Error
createMethodFilterInterceptor({ 'SomeService': [auth] });

Practical Example: Different Resilience Per Method ​

typescript
import {
  createDefaultInterceptors,
  createMethodFilterInterceptor,
  createTimeoutInterceptor,
  createCircuitBreakerInterceptor,
} from '@connectum/interceptors';

const resilience = createMethodFilterInterceptor({
  // Fast reads: 5 second timeout
  'catalog.v1.CatalogService/GetProduct': [
    createTimeoutInterceptor({ duration: 5_000 }),
  ],

  // Heavy reports: 60 second timeout
  'report.v1.ReportService/*': [
    createTimeoutInterceptor({ duration: 60_000 }),
  ],

  // Admin mutations: audit logging
  'admin.v1.AdminService/*': [
    createAuditLogInterceptor(),
  ],
});

const server = createServer({
  services: [routes],
  interceptors: [
    // Structural defaults; avoid a global timeout that would cap the 60s rule
    ...createDefaultInterceptors(),
    resilience,
  ],
});

Approach 3: Custom Interceptor with Manual Filtering ​

For dynamic or complex filtering logic that does not fit wildcard patterns, write a custom interceptor:

typescript
import type { Interceptor } from '@connectrpc/connect';

const conditionalAuth: Interceptor = (next) => async (req) => {
  // Dynamic condition: check service name at runtime
  if (req.service.typeName === 'admin.v1.AdminService') {
    await verifyAdminToken(req);
  }

  // Check method kind
  if (req.method.methodKind === 'server_streaming') {
    attachStreamMonitoring(req);
  }

  return next(req);
};

Use this approach for filtering based on request content, runtime state, or conditions that cannot be expressed as static patterns.

skip* Options on Built-in Interceptors ​

The built-in resilience interceptors have skip* options that serve a different purpose from method filtering. These are technical constraints, not routing concerns:

OptionUsed byWhy
skipStreamingtimeout, bulkhead, circuitBreaker, retry, fallbackResilience patterns wrap the full call. You cannot retry a stream, timeout a long-lived connection, or replace a stream with a fallback value.
skipGrpcServicesserializerSkips operational services whose names start with grpc.; it does not select by wire protocol.
skipHealthCheckloggerConvenience shortcut to reduce log noise from frequent health checks.

These options complement createMethodFilterInterceptor. Method filtering handles business routing ("which interceptors for which methods"), while skip* handles technical limitations ("this interceptor cannot operate on this call type").

Choosing the Right Approach ​

ScenarioApproach
Interceptor tied to a specific servicedefineService(..., { interceptors })
Declarative routing by pattern across multiple servicescreateMethodFilterInterceptor
Dynamic logic based on request content or runtime stateCustom interceptor with manual filtering
Technical limitation (streaming, binary protocol)skip* options on built-in interceptors

WARNING

Do not confuse skip* options with method filtering. Setting skipStreaming: true on the retry interceptor means "retry cannot handle streams" -- it is not a routing decision. Use createMethodFilterInterceptor or router-level interceptors for business routing.