Por qué necesitas django-filter
Una API REST que solo permite GET /productos/ con todos los resultados se queda corta rápido. Los consumidores piden:
?categoria_id=5(filtro exacto).?precio_min=100&precio_max=500(rango).?nombre=laptop(búsqueda parcial case-insensitive).?fecha_desde=2026-01-01&fecha_hasta=2026-12-31(rango fechas).?ids=1,2,3(filtro por lista).?ordering=-precio(ordenación).
Programar todo esto a mano en cada ViewSet es tedioso y propenso a errores. django-filter lo resuelve con clases declarativas que DRF integra automáticamente.
Instalación
pip install django-filter
# settings.py
INSTALLED_APPS = [..., 'django_filters']
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
Con esto cualquier ViewSet ya puede usar filtros.
Filtros simples con filterset_fields
Para casos sencillos, lista los campos:
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Producto.objects.all()
serializer_class = ProductoSerializer
filterset_fields = ['categoria', 'disponible', 'marca']
Queries soportadas:
GET /api/productos/?categoria=5
GET /api/productos/?categoria=5&disponible=true
GET /api/productos/?marca=Samsung&disponible=false
Los filtros son igualdad exacta sobre esos campos.
FilterSet con tipos de filtro
Para casos complejos, declara una FilterSet:
# filters.py
import django_filters
from django_filters import rest_framework as filters
from .models import Producto
class ProductoFilter(filters.FilterSet):
# Búsqueda parcial case-insensitive
nombre = django_filters.CharFilter(lookup_expr='icontains')
# Rango de precio
precio_min = django_filters.NumberFilter(field_name='precio', lookup_expr='gte')
precio_max = django_filters.NumberFilter(field_name='precio', lookup_expr='lte')
# Fechas de rango
creado_desde = django_filters.DateFilter(field_name='creado_en', lookup_expr='gte')
creado_hasta = django_filters.DateFilter(field_name='creado_en', lookup_expr='lte')
# Booleano
disponible = django_filters.BooleanFilter()
# Choice (desde un enum de modelo)
estado = django_filters.ChoiceFilter(choices=Producto.ESTADOS)
# Lista de IDs (?ids=1,2,3)
ids = django_filters.BaseInFilter(field_name='id', lookup_expr='in')
# Relaciones
categoria_nombre = django_filters.CharFilter(
field_name='categoria__nombre',
lookup_expr='iexact',
)
class Meta:
model = Producto
fields = ['nombre', 'precio_min', 'precio_max', 'categoria',
'creado_desde', 'creado_hasta', 'disponible', 'estado', 'ids']
En el ViewSet:
from .filters import ProductoFilter
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Producto.objects.all()
serializer_class = ProductoSerializer
filterset_class = ProductoFilter
Queries:
GET /api/productos/?nombre=lap&precio_min=500&precio_max=2000&disponible=true
GET /api/productos/?creado_desde=2026-01-01&creado_hasta=2026-12-31
GET /api/productos/?ids=1,5,8,13
Tipos de filtro más útiles
| Filter | Uso | Ejemplo query |
|--------|-----|---------------|
| CharFilter | Texto con lookup_expr | ?nombre=lap con icontains → LIKE '%lap%' |
| NumberFilter | Números con gte/lte/exact | ?precio_min=100 |
| BooleanFilter | true/false | ?activo=true |
| DateFilter | Fecha exacta o rango | ?fecha=2026-04-18 |
| DateFromToRangeFilter | Rango con _after/_before | ?fecha_after=2026-01-01&fecha_before=2026-12-31 |
| NumericRangeFilter | Rango numérico | ?precio_min=100&precio_max=500 |
| ChoiceFilter | Lista de opciones predefinidas | ?estado=activo |
| MultipleChoiceFilter | Varias opciones | ?estados=activo&estados=revision |
| BaseInFilter | Lista separada por coma | ?ids=1,2,3 |
| BaseRangeFilter | Rango separado por coma | ?precio_range=100,500 |
| ModelChoiceFilter | FK a otro modelo | ?categoria=5 (resuelve a Categoria.objects.get) |
| ModelMultipleChoiceFilter | M2M | ?tags=1&tags=2 |
Filtros custom con method
Cuando la lógica es compleja, define un método:
class ProductoFilter(filters.FilterSet):
disponible_en_stock = django_filters.BooleanFilter(method='filter_en_stock')
def filter_en_stock(self, queryset, name, value):
if value:
return queryset.filter(stock__gt=0, disponible=True)
return queryset.filter(stock=0) | queryset.filter(disponible=False)
# Filtro custom que combina varias columnas
busqueda = django_filters.CharFilter(method='filter_busqueda')
def filter_busqueda(self, queryset, name, value):
return queryset.filter(
Q(nombre__icontains=value) |
Q(descripcion__icontains=value) |
Q(sku__iexact=value)
)
Uso: ?disponible_en_stock=true o ?busqueda=laptop.
SearchFilter: búsqueda full-text simple
Independiente de django-filter, DRF ofrece SearchFilter para búsquedas sobre varias columnas:
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Producto.objects.all()
serializer_class = ProductoSerializer
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filterset_class = ProductoFilter
search_fields = ['nombre', 'descripcion', 'sku']
Uso: ?search=laptop busca en todos los search_fields con icontains (o istartswith si el campo empieza con ^, iexact si con =).
Para búsquedas realmente potentes (relevancia, stemming) usa PostgreSQL full-text o Elasticsearch.
OrderingFilter: ordenación dinámica
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ['precio', 'creado_en', 'nombre']
ordering = ['-creado_en'] # default
Uso: ?ordering=precio (ascendente) o ?ordering=-precio (descendente). Varios: ?ordering=-precio,nombre.
Limitar a un set concreto:
ordering_fields = ['precio', 'creado_en'] # solo estos
O permitir ordenación sobre campos anotados:
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Producto.objects.annotate(total_vendido=Count('ventas'))
ordering_fields = ['precio', 'total_vendido']
Combinar filtros con permisos (multi-tenant)
Si la API es multi-tenant y el usuario solo ve sus productos, combina get_queryset con filtros:
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProductoSerializer
filterset_class = ProductoFilter
def get_queryset(self):
return (
Producto.objects
.filter(tenant=self.request.user.tenant) # filtro base obligatorio
.select_related('categoria')
.prefetch_related('tags')
)
El usuario puede aplicar cualquier filtro de ProductoFilter, pero siempre limitado a su tenant.
Documentación automática con drf-spectacular
Los filtros de django-filter se documentan automáticamente en Swagger UI:
GET /api/productos/?
nombre string (query, optional)
precio_min number (query, optional)
precio_max number (query, optional)
disponible boolean (query, optional)
ordering string (query, optional) [precio, -precio, nombre, -nombre]
No requiere configuración extra. El schema OpenAPI es correcto y los integradores lo ven desde Swagger UI y pueden probarlo interactivamente.
Paginación combinada con filtros
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50,
}
Query: ?nombre=lap&precio_min=500&page=2&page_size=25 — se aplican los filtros y luego se pagina.
Para datasets grandes, cursor pagination escala mejor:
class ProductoPagination(CursorPagination):
page_size = 50
ordering = '-creado_en'
cursor_query_param = 'cursor'
class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
pagination_class = ProductoPagination
Rendimiento de filtros
- Indexa los campos filtrables más usados:
CREATE INDEX idx_producto_categoria ON producto(categoria_id);. - Filtros con
icontains(LIKE '%texto%') no usan índice B-tree estándar. Para búsquedas full-text, usarpg_trgmcon GIN o full-text search. - Filtros con OR o
Q(...) | Q(...)pueden ser lentos. A veces dos queries UNION ALL son más eficientes. - Limitar la cardinalidad: siempre ordering + limit para evitar "SELECT * FROM tabla_enorme".
-- Crear indices para los filtros mas frecuentes
CREATE INDEX CONCURRENTLY idx_producto_categoria ON producto(categoria_id);
CREATE INDEX CONCURRENTLY idx_producto_precio ON producto(precio) WHERE disponible = true;
CREATE INDEX CONCURRENTLY idx_producto_nombre_trgm ON producto USING GIN (nombre gin_trgm_ops);
El último necesita CREATE EXTENSION pg_trgm; para búsquedas icontains rápidas.
Checklist
- [ ]
DEFAULT_FILTER_BACKENDSconfigurado en settings. - [ ] FilterSet por modelo con los filtros documentados.
- [ ] SearchFilter con
search_fieldsde las columnas más buscadas. - [ ] OrderingFilter con
ordering_fieldsexplícito (evita ordenar por columnas no indexadas). - [ ] get_queryset con
select_related/prefetch_relatedapropiados. - [ ] Índices en columnas filtrables más frecuentes.
- [ ] Paginación activada para endpoints de listado.
- [ ] Tests que cubren combinaciones de filtros.
- [ ] Documentación Swagger muestra los filtros correctamente.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Django
Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Instalar django-filter e integrar DjangoFilterBackend. Declarar una FilterSet con los tipos de filtros mas comunes. Usar filtros por rango (DateFromToRangeFilter, NumericRangeFilter), IN (BaseInFilter), case-insensitive (icontains). Escribir filtros custom con un method=. Combinar con SearchFilter y OrderingFilter para busquedas full-text y ordenacion dinamica.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje