🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 2 - Aula 3

Context e Middleware

Domine Context e Middleware no tRPC: autenticação robusta, logging profissional, rate limiting e proteção de rotas avançada.

50 min
Avançado
Middleware

🎯 Por que Context e Middleware são fundamentais?

Segurança: Middleware protege rotas e garante que apenas usuários autorizados acessem dados.

Observabilidade: Context permite logging, métricas e debugging em todas as camadas.

🔧 Context Avançado

src/server/trpc/context.ts
// 📁 src/server/trpc/context.ts
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { type Session } from 'next-auth';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/config';
import { prisma } from '../db/client';

// 🎯 Tipo do contexto
interface CreateContextOptions {
  session: Session | null;
  req: CreateNextContextOptions['req'];
  res: CreateNextContextOptions['res'];
}

// 🔧 Criar contexto interno (para testes)
const createInnerTRPCContext = (opts: CreateContextOptions) => {
  return {
    session: opts.session,
    prisma,
    req: opts.req,
    res: opts.res,
    
    // 📊 Informações úteis sobre a requisição
    userAgent: opts.req.headers['user-agent'],
    ip: opts.req.headers['x-forwarded-for'] || 
        opts.req.headers['x-real-ip'] || 
        opts.req.connection?.remoteAddress,
    
    // ⏰ Timestamp da requisição
    requestId: Math.random().toString(36).substring(7),
    timestamp: new Date(),
  };
};

// 🌐 Criar contexto para Next.js API routes
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
  const { req, res } = opts;

  // 🔐 Buscar sessão do usuário
  const session = await getServerSession(req, res, authOptions);

  return createInnerTRPCContext({
    session,
    req,
    res,
  });
};

export type Context = Awaited<ReturnType<typeof createTRPCContext>>;

🛡️ Middleware de Autenticação

src/server/trpc/trpc.ts
// 📁 src/server/trpc/trpc.ts
import { TRPCError, initTRPC } from '@trpc/server';
import { type Context } from './context';
import superjson from 'superjson';

// 🚀 Inicializar tRPC com contexto
const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const router = t.router;
export const publicProcedure = t.procedure;

// 📝 Middleware de logging
const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
  const start = Date.now();
  
  console.log(`📡 tRPC ${type.toUpperCase()} ${path} - Request ID: ${ctx.requestId}`);
  console.log(`👤 User: ${ctx.session?.user?.email || 'anonymous'}`);
  console.log(`🌐 IP: ${ctx.ip}`);

  const result = await next();

  const durationMs = Date.now() - start;
  
  if (result.ok) {
    console.log(`✅ ${path} completed in ${durationMs}ms`);
  } else {
    console.error(`❌ ${path} failed in ${durationMs}ms:`, result.error);
  }

  return result;
});

// 🔐 Middleware de autenticação
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Você precisa estar logado para acessar este recurso',
    });
  }

  return next({
    ctx: {
      // ✅ Infers the session as non-nullable
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

// 👑 Middleware de admin
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Acesso negado: login necessário',
    });
  }

  if (ctx.session.user.role !== 'ADMIN') {
    throw new TRPCError({
      code: 'FORBIDDEN',
      message: 'Acesso negado: privilégios de administrador necessários',
    });
  }

  return next({
    ctx: {
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

// 🚦 Rate limiting middleware
const rateLimitMiddleware = t.middleware(async ({ ctx, next, path }) => {
  const key = `rate_limit:${ctx.ip}:${path}`;
  
  // 🔍 Verificar rate limit (implementação com Redis seria ideal)
  const requests = await checkRateLimit(key);
  
  if (requests > 100) { // 100 requests por minuto
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'Muitas requisições. Tente novamente em 1 minuto.',
    });
  }

  return next();
});

// 📦 Procedures com middleware
export const loggedProcedure = publicProcedure
  .use(loggingMiddleware)
  .use(rateLimitMiddleware);

export const protectedProcedure = loggedProcedure
  .use(enforceUserIsAuthed);

export const adminProcedure = loggedProcedure
  .use(enforceUserIsAdmin);

// 🔧 Helper para rate limiting (versão simples em memória)
const rateLimit = new Map<string, { count: number; resetTime: number }>();

async function checkRateLimit(key: string): Promise<number> {
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 minuto
  
  const current = rateLimit.get(key);
  
  if (!current || now > current.resetTime) {
    rateLimit.set(key, { count: 1, resetTime: now + windowMs });
    return 1;
  }
  
  current.count++;
  return current.count;
}

⚡ Middleware Customizado Avançado

src/server/trpc/middleware/validation.ts
// 📁 src/server/trpc/middleware/validation.ts
import { TRPCError } from '@trpc/server';
import { t } from '../trpc';

// 🔍 Middleware de validação de ownership
export const ownershipMiddleware = <T extends { userId: string }>(
  resourceGetter: (ctx: any, input: any) => Promise<T | null>
) => {
  return t.middleware(async ({ ctx, input, next }) => {
    if (!ctx.session?.user?.id) {
      throw new TRPCError({
        code: 'UNAUTHORIZED',
        message: 'Autenticação necessária',
      });
    }

    const resource = await resourceGetter(ctx, input);
    
    if (!resource) {
      throw new TRPCError({
        code: 'NOT_FOUND',
        message: 'Recurso não encontrado',
      });
    }

    if (resource.userId !== ctx.session.user.id) {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'Você não tem permissão para acessar este recurso',
      });
    }

    return next({
      ctx: {
        ...ctx,
        resource,
      },
    });
  });
};

// 📊 Middleware de métricas
export const metricsMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
  const startTime = process.hrtime.bigint();
  
  try {
    const result = await next();
    
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - startTime) / 1_000_000; // Convert to ms
    
    // 📈 Enviar métricas (exemplo com console, mas use Prometheus/DataDog)
    console.log(`📊 METRIC: ${type}.${path} duration=${duration}ms status=success user=${ctx.session?.user?.id || 'anonymous'}`);
    
    return result;
  } catch (error) {
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - startTime) / 1_000_000;
    
    console.log(`📊 METRIC: ${type}.${path} duration=${duration}ms status=error user=${ctx.session?.user?.id || 'anonymous'}`);
    
    throw error;
  }
});

// 🔒 Middleware de sanitização
export const sanitizeMiddleware = t.middleware(async ({ input, next }) => {
  // 🧹 Sanitizar inputs de string
  const sanitizeString = (str: string) => {
    return str
      .trim()
      .replace(/<script[^>]*>.*?</script>/gi, '') // Remove scripts
      .replace(/<[^>]*>/g, ''); // Remove HTML tags
  };

  const sanitizeObject = (obj: any): any => {
    if (typeof obj === 'string') {
      return sanitizeString(obj);
    }
    
    if (Array.isArray(obj)) {
      return obj.map(sanitizeObject);
    }
    
    if (obj && typeof obj === 'object') {
      const sanitized: any = {};
      for (const [key, value] of Object.entries(obj)) {
        sanitized[key] = sanitizeObject(value);
      }
      return sanitized;
    }
    
    return obj;
  };

  const sanitizedInput = sanitizeObject(input);

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

🚀 Uso Prático em Routers

src/server/trpc/routers/user.ts
// 📁 src/server/trpc/routers/user.ts
import { z } from 'zod';
import { router, protectedProcedure, adminProcedure } from '../trpc';
import { ownershipMiddleware, metricsMiddleware } from '../middleware/validation';

export const userRouter = router({
  // 👤 Buscar perfil do usuário logado
  getProfile: protectedProcedure
    .use(metricsMiddleware)
    .query(async ({ ctx }) => {
      // ✅ ctx.session.user é tipado e não-null aqui
      const user = await ctx.prisma.user.findUnique({
        where: { id: ctx.session.user.id },
        select: {
          id: true,
          name: true,
          email: true,
          role: true,
          createdAt: true,
          _count: {
            select: {
              posts: true,
              comments: true,
              likes: true,
            },
          },
        },
      });

      return user;
    }),

  // ✏️ Atualizar perfil com ownership
  updateProfile: protectedProcedure
    .use(metricsMiddleware)
    .use(ownershipMiddleware(async (ctx, input: { userId: string }) => {
      return await ctx.prisma.user.findUnique({
        where: { id: input.userId },
      });
    }))
    .input(z.object({
      userId: z.string(),
      name: z.string().min(2).max(50).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { userId, ...updateData } = input;

      // 🔄 Atualizar apenas campos fornecidos
      const updatedUser = await ctx.prisma.user.update({
        where: { id: userId },
        data: updateData,
        select: {
          id: true,
          name: true,
          email: true,
          updatedAt: true,
        },
      });

      return updatedUser;
    }),

  // 👥 Listar todos os usuários (apenas admin)
  getAll: adminProcedure
    .use(metricsMiddleware)
    .input(z.object({
      page: z.number().min(1).default(1),
      limit: z.number().min(1).max(100).default(10),
      search: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { page, limit, search } = input;
      const skip = (page - 1) * limit;

      const where = search ? {
        OR: [
          { name: { contains: search, mode: 'insensitive' as const } },
          { email: { contains: search, mode: 'insensitive' as const } },
        ],
      } : {};

      const [users, total] = await Promise.all([
        ctx.prisma.user.findMany({
          where,
          skip,
          take: limit,
          select: {
            id: true,
            name: true,
            email: true,
            role: true,
            createdAt: true,
            _count: {
              select: {
                posts: true,
                comments: true,
              },
            },
          },
          orderBy: { createdAt: 'desc' },
        }),
        ctx.prisma.user.count({ where }),
      ]);

      return {
        users,
        pagination: {
          page,
          limit,
          total,
          pages: Math.ceil(total / limit),
        },
      };
    }),
});

💡 Melhores Práticas para Middleware

Ordem Importa:Execute logging primeiro, depois autenticação, depois validação.

Fail Fast:Middleware de autenticação deve falhar rapidamente para economizar recursos.

Composição:Combine middlewares pequenos e específicos ao invés de um grande.

Observabilidade:Sempre inclua logging e métricas para debugging em produção.