Domine queries avançadas no tRPC: paginação infinita, filtros complexos, ordenação customizada e otimizações de performance.
Performance: Paginação e filtros reduzem latência e consumo de memória drasticamente.
UX Superior: Busca rápida e navegação fluida mantém usuários engajados.
// 📁 src/server/trpc/routers/post.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const postRouter = router({
// 📋 Paginação cursor-based infinita
getInfinite: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(), // 🔍 Cursor para paginação
search: z.string().optional(),
categoryId: z.string().optional(),
sortBy: z.enum(['createdAt', 'title', 'views']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
}))
.query(async ({ input, ctx }) => {
const { limit, cursor, search, categoryId, sortBy, sortOrder } = input;
// 🔍 Construir filtros dinamicamente
const whereClause: any = {
published: true,
...(search && {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
{ author: { name: { contains: search, mode: 'insensitive' } } },
],
}),
...(categoryId && { categoryId }),
};
// 🚀 Query otimizada com cursor
const posts = await ctx.prisma.post.findMany({
take: limit + 1, // ✨ Buscar um a mais para verificar se há próxima página
cursor: cursor ? { id: cursor } : undefined,
where: whereClause,
orderBy: {
[sortBy]: sortOrder,
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
category: {
select: {
id: true,
name: true,
slug: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
});
// 📊 Determinar próximo cursor
let nextCursor: typeof cursor | undefined = undefined;
if (posts.length > limit) {
const nextItem = posts.pop(); // 🔄 Remove o item extra
nextCursor = nextItem!.id;
}
return {
posts,
nextCursor,
hasMore: !!nextCursor,
};
}),
// 📈 Busca com facets e agregações
search: publicProcedure
.input(z.object({
query: z.string().min(1),
filters: z.object({
categories: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
dateRange: z.object({
start: z.date().optional(),
end: z.date().optional(),
}).optional(),
tags: z.array(z.string()).optional(),
}).optional(),
limit: z.number().min(1).max(50).default(20),
page: z.number().min(1).default(1),
}))
.query(async ({ input, ctx }) => {
const { query, filters, limit, page } = input;
const skip = (page - 1) * limit;
// 🔍 Construir query complexa
const whereClause: any = {
published: true,
AND: [
{
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } },
{ excerpt: { contains: query, mode: 'insensitive' } },
],
},
...(filters?.categories && filters.categories.length > 0
? [{ categoryId: { in: filters.categories } }]
: []),
...(filters?.authors && filters.authors.length > 0
? [{ authorId: { in: filters.authors } }]
: []),
...(filters?.dateRange?.start
? [{ createdAt: { gte: filters.dateRange.start } }]
: []),
...(filters?.dateRange?.end
? [{ createdAt: { lte: filters.dateRange.end } }]
: []),
...(filters?.tags && filters.tags.length > 0
? [{
tags: {
some: {
tag: {
name: { in: filters.tags }
}
}
}
}]
: []),
],
};
// 📊 Execução paralela de queries
const [posts, totalCount] = await Promise.all([
// 🔍 Posts principais
ctx.prisma.post.findMany({
where: whereClause,
skip,
take: limit,
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
category: true,
tags: {
include: {
tag: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
orderBy: [
{ createdAt: 'desc' },
],
}),
// 🔢 Contagem total
ctx.prisma.post.count({ where: whereClause }),
]);
return {
posts,
pagination: {
page,
limit,
total: totalCount,
pages: Math.ceil(totalCount / limit),
hasMore: skip + limit < totalCount,
},
};
}),
});
// 📁 src/server/trpc/routers/product.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
// 🎯 Schema de filtros complexos
const productFiltersSchema = z.object({
search: z.string().optional(),
category: z.string().optional(),
priceRange: z.object({
min: z.number().min(0).optional(),
max: z.number().min(0).optional(),
}).optional(),
rating: z.number().min(1).max(5).optional(),
inStock: z.boolean().optional(),
brands: z.array(z.string()).optional(),
attributes: z.record(z.string(), z.array(z.string())).optional(),
sortBy: z.enum(['name', 'price', 'rating', 'created', 'popular']).default('created'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
export const productRouter = router({
// 🛍️ Busca de produtos com filtros avançados
getFiltered: publicProcedure
.input(z.object({
filters: productFiltersSchema,
pagination: z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(50).default(20),
}),
}))
.query(async ({ input, ctx }) => {
const { filters, pagination } = input;
const { page, limit } = pagination;
const skip = (page - 1) * limit;
// 🔧 Função para construir where clause dinamicamente
const buildWhereClause = () => {
const conditions: any[] = [];
// 🔍 Busca por texto
if (filters.search) {
conditions.push({
OR: [
{ name: { contains: filters.search, mode: 'insensitive' } },
{ description: { contains: filters.search, mode: 'insensitive' } },
{ brand: { name: { contains: filters.search, mode: 'insensitive' } } },
],
});
}
// 📂 Filtro por categoria
if (filters.category) {
conditions.push({
categoryId: filters.category,
});
}
// 💰 Filtro por faixa de preço
if (filters.priceRange) {
const priceCondition: any = {};
if (filters.priceRange.min !== undefined) {
priceCondition.gte = filters.priceRange.min;
}
if (filters.priceRange.max !== undefined) {
priceCondition.lte = filters.priceRange.max;
}
if (Object.keys(priceCondition).length > 0) {
conditions.push({ price: priceCondition });
}
}
// ⭐ Filtro por rating
if (filters.rating) {
conditions.push({
averageRating: { gte: filters.rating },
});
}
// 📦 Filtro por estoque
if (filters.inStock !== undefined) {
conditions.push({
stockQuantity: filters.inStock ? { gt: 0 } : { lte: 0 },
});
}
// 🏷️ Filtro por marcas
if (filters.brands && filters.brands.length > 0) {
conditions.push({
brand: {
name: { in: filters.brands },
},
});
}
// 🔧 Filtros por atributos customizados
if (filters.attributes && Object.keys(filters.attributes).length > 0) {
Object.entries(filters.attributes).forEach(([key, values]) => {
if (values.length > 0) {
conditions.push({
attributes: {
some: {
name: key,
value: { in: values },
},
},
});
}
});
}
return conditions.length > 0 ? { AND: conditions } : {};
};
// 📊 Função para construir orderBy
const buildOrderBy = () => {
const orderMap = {
name: 'name',
price: 'price',
rating: 'averageRating',
created: 'createdAt',
popular: 'viewCount',
};
return {
[orderMap[filters.sortBy]]: filters.sortOrder,
};
};
const whereClause = buildWhereClause();
const orderBy = buildOrderBy();
// 🚀 Executar queries em paralelo
const [products, totalCount, aggregations] = await Promise.all([
// 🛍️ Produtos principais
ctx.prisma.product.findMany({
where: whereClause,
skip,
take: limit,
orderBy,
include: {
brand: {
select: {
id: true,
name: true,
logo: true,
},
},
category: {
select: {
id: true,
name: true,
slug: true,
},
},
images: {
take: 3,
select: {
url: true,
alt: true,
},
},
attributes: {
select: {
name: true,
value: true,
},
},
_count: {
select: {
reviews: true,
orders: true,
},
},
},
}),
// 🔢 Contagem total
ctx.prisma.product.count({ where: whereClause }),
// 📊 Agregações para filtros
ctx.prisma.product.aggregate({
where: whereClause,
_min: {
price: true,
},
_max: {
price: true,
},
_avg: {
price: true,
averageRating: true,
},
}),
]);
return {
products,
pagination: {
page,
limit,
total: totalCount,
pages: Math.ceil(totalCount / limit),
hasMore: skip + limit < totalCount,
},
aggregations: {
priceRange: {
min: aggregations._min.price || 0,
max: aggregations._max.price || 0,
},
averagePrice: aggregations._avg.price || 0,
averageRating: aggregations._avg.averageRating || 0,
},
};
}),
});
// 📁 src/components/products/product-list.tsx
'use client';
import { useState, useMemo } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { trpc } from '@/lib/trpc';
import { ProductCard } from './product-card';
import { ProductFilters } from './product-filters';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
interface ProductListProps {
initialFilters?: {
category?: string;
search?: string;
};
}
export function ProductList({ initialFilters = {} }: ProductListProps) {
// 🎯 Estado dos filtros
const [filters, setFilters] = useState({
search: initialFilters.search || '',
category: initialFilters.category || '',
priceRange: { min: undefined, max: undefined },
rating: undefined,
inStock: undefined,
brands: [],
attributes: {},
sortBy: 'created' as const,
sortOrder: 'desc' as const,
});
// 📊 Query infinita para produtos
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
error,
} = trpc.post.getInfinite.useInfiniteQuery(
{
...filters,
limit: 20,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
}
);
// 📊 Produtos flattened de todas as páginas
const products = useMemo(() => {
return data?.pages.flatMap((page) => page.posts) || [];
}, [data]);
// 🔄 Função para atualizar filtros
const updateFilters = (newFilters: Partial<typeof filters>) => {
setFilters((prev) => ({
...prev,
...newFilters,
}));
};
// 🔄 Função para limpar filtros
const clearFilters = () => {
setFilters({
search: '',
category: '',
priceRange: { min: undefined, max: undefined },
rating: undefined,
inStock: undefined,
brands: [],
attributes: {},
sortBy: 'created',
sortOrder: 'desc',
});
};
// 🔄 Função para carregar mais produtos
const loadMore = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<LoadingSpinner size="lg" />
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<p className="text-red-400">Erro ao carregar produtos: {error.message}</p>
</div>
);
}
return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* 🔍 Sidebar de filtros */}
<div className="lg:col-span-1">
<ProductFilters
filters={filters}
onUpdateFilters={updateFilters}
onClearFilters={clearFilters}
/>
</div>
{/* 📋 Lista de produtos */}
<div className="lg:col-span-3">
{/* 📊 Cabeçalho com resultados */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">
Produtos {products.length > 0 && (
<span className="text-gray-400">({products.length})</span>
)}
</h2>
{/* 🔄 Ordenação */}
<select
value={filters.sortBy + '-' + filters.sortOrder}
onChange={(e) => {
const [sortBy, sortOrder] = e.target.value.split('-');
updateFilters({
sortBy: sortBy as any,
sortOrder: sortOrder as any
});
}}
className="bg-gray-800 text-white border border-gray-600 rounded px-3 py-2"
>
<option value="created-desc">Mais Recentes</option>
<option value="created-asc">Mais Antigos</option>
<option value="title-asc">A-Z</option>
<option value="title-desc">Z-A</option>
<option value="views-desc">Mais Visualizados</option>
</select>
</div>
{/* 🛍️ Grid de produtos */}
{products.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-400">Nenhum produto encontrado</p>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
{/* 📄 Botão para carregar mais */}
{hasNextPage && (
<div className="text-center mt-8">
<button
onClick={loadMore}
disabled={isFetchingNextPage}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
{isFetchingNextPage ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
Carregando...
</>
) : (
'Carregar Mais'
)}
</button>
</div>
)}
</>
)}
</div>
</div>
);
}
// 📁 src/server/trpc/routers/optimized-queries.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const optimizedRouter = router({
// 🚀 Query com cache inteligente
getPostsWithCache: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
tags: z.array(z.string()).optional(),
}))
.query(async ({ input, ctx }) => {
const { limit, cursor, tags } = input;
// 🔑 Chave de cache baseada nos parâmetros
const cacheKey = 'posts-' + JSON.stringify({ limit, cursor, tags });
// 📊 Verificar cache Redis/Memcached
const cached = await ctx.cache?.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 🗄️ Query otimizada com select específico
const posts = await ctx.prisma.post.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
where: tags ? {
tags: {
some: {
tag: {
name: { in: tags },
},
},
},
} : undefined,
// 🎯 Selecionar apenas campos necessários
select: {
id: true,
title: true,
excerpt: true,
slug: true,
createdAt: true,
updatedAt: true,
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
category: {
select: {
id: true,
name: true,
slug: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
let nextCursor: string | undefined;
if (posts.length > limit) {
const nextItem = posts.pop();
nextCursor = nextItem!.id;
}
const result = {
posts,
nextCursor,
hasMore: !!nextCursor,
};
// 💾 Salvar no cache por 5 minutos
await ctx.cache?.set(cacheKey, JSON.stringify(result), 300);
return result;
}),
// 📊 Agregações otimizadas
getPostStats: publicProcedure
.input(z.object({
authorId: z.string().optional(),
categoryId: z.string().optional(),
dateRange: z.object({
start: z.date(),
end: z.date(),
}).optional(),
}))
.query(async ({ input, ctx }) => {
const { authorId, categoryId, dateRange } = input;
// 🔍 Construir filtros
const whereClause: any = {
published: true,
...(authorId && { authorId }),
...(categoryId && { categoryId }),
...(dateRange && {
createdAt: {
gte: dateRange.start,
lte: dateRange.end,
},
}),
};
// 📊 Executar agregações em paralelo
const [
totalPosts,
totalViews,
totalComments,
totalLikes,
topAuthors,
topCategories,
] = await Promise.all([
// 📝 Total de posts
ctx.prisma.post.count({ where: whereClause }),
// 👀 Total de visualizações
ctx.prisma.post.aggregate({
where: whereClause,
_sum: {
views: true,
},
}),
// 💬 Total de comentários
ctx.prisma.comment.count({
where: {
post: whereClause,
},
}),
// ❤️ Total de curtidas
ctx.prisma.like.count({
where: {
post: whereClause,
},
}),
// 👥 Top autores
ctx.prisma.user.findMany({
where: {
posts: {
some: whereClause,
},
},
select: {
id: true,
name: true,
avatar: true,
_count: {
select: {
posts: true,
},
},
},
orderBy: {
posts: {
_count: 'desc',
},
},
take: 5,
}),
// 📂 Top categorias
ctx.prisma.category.findMany({
where: {
posts: {
some: whereClause,
},
},
select: {
id: true,
name: true,
slug: true,
_count: {
select: {
posts: true,
},
},
},
orderBy: {
posts: {
_count: 'desc',
},
},
take: 5,
}),
]);
return {
overview: {
totalPosts,
totalViews: totalViews._sum.views || 0,
totalComments,
totalLikes,
},
topAuthors,
topCategories,
};
}),
});
Select Específico:Sempre use select para buscar apenas os campos necessários.
Índices de Banco:Crie índices para campos usados em WHERE e ORDER BY.
Cache Inteligente:Use Redis para cachear queries complexas e agregações.
Paginação Cursor:Prefira cursor-based para melhor performance em grandes datasets.