Domine Context e Middleware no tRPC: autenticação robusta, logging profissional, rate limiting e proteção de rotas avançada.
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.
// 📁 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>>;
// 📁 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;
}
// 📁 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,
},
});
});
// 📁 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),
},
};
}),
});
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.