7 KiB
7 KiB
API Contract — GET /api/v1/properties
Feature: 023-ux-melhorias-imoveis
Versão: extensão da rota existente — sem breaking changes
Arquivo: backend/app/routes/properties.py
Descrição
Rota de listagem paginada de imóveis. Esta feature adiciona dois novos parâmetros query opcionais (q e sort) à rota existente. Todos os parâmetros existentes são preservados sem alteração.
Request
GET /api/v1/properties
Query Parameters
Parâmetros novos (adicionados nesta feature)
| Parâmetro | Tipo | Obrigatório | Valores | Default | Descrição |
|---|---|---|---|---|---|
q |
string |
Não | qualquer string, máx 200 chars | — (sem filtro textual) | Busca case-insensitive em title, address, code, neighborhood.name |
sort |
string |
Não | relevance · price_asc · price_desc · area_desc · newest |
relevance |
Critério de ordenação dos resultados |
Parâmetros existentes (preservados)
| Parâmetro | Tipo | Descrição |
|---|---|---|
listing_type |
'venda' | 'aluguel' |
Tipo de negócio |
subtype_id |
integer |
ID do subtipo de imóvel |
city_id |
integer |
ID da cidade |
neighborhood_id |
integer |
ID do bairro |
imobiliaria_id |
integer |
ID da imobiliária |
price_min |
number |
Preço mínimo |
price_max |
number |
Preço máximo |
include_condo |
'true' |
Incluir condomínio no cálculo de preço |
bedrooms_min |
integer |
Mínimo de quartos |
bedrooms_max |
integer |
Máximo de quartos |
bathrooms_min |
integer |
Mínimo de banheiros |
bathrooms_max |
integer |
Máximo de banheiros |
parking_min |
integer |
Mínimo de vagas |
parking_max |
integer |
Máximo de vagas |
area_min |
integer |
Área mínima (m²) |
area_max |
integer |
Área máxima (m²) |
amenity_ids |
string (lista separada por vírgula) |
IDs de comodidades (AND lógico) |
page |
integer |
Página atual (default: 1) |
per_page |
integer |
Resultados por página (default: 24, max: 48) |
Response
200 OK
Sem alterações no schema de resposta existente.
{
"items": [
{
"id": "uuid",
"title": "Apartamento Jardins 2 quartos",
"slug": "apartamento-jardins-2-quartos",
"address": "Rua Oscar Freire, 123",
"code": "AP-0042",
"price": "3500.00",
"type": "aluguel",
"bedrooms": 2,
"bathrooms": 1,
"parking_spots": 1,
"area_m2": 75,
"is_featured": false,
"created_at": "2026-04-11T14:30:00",
"photos": [
{ "url": "/imoveis/ap-0042/foto1.jpg", "alt_text": "Sala de estar" }
],
"subtype": { "id": 1, "name": "Apartamento" },
"city": { "id": 1, "name": "São Paulo" },
"neighborhood": { "id": 5, "name": "Jardins" }
}
],
"total": 42,
"page": 1,
"per_page": 16,
"pages": 3
}
Erros
| Status | Condição |
|---|---|
400 |
Não aplicável — parâmetros inválidos são ignorados silenciosamente (comportamento existente) |
500 |
Erro interno do servidor |
Semântica dos Novos Parâmetros
q — Busca Textual
- Campos buscados:
title,address,code,neighborhood.name - Operador:
ILIKE '%termo%'(case-insensitive, busca parcial) - Lógica: OR entre os campos (
title ILIKE $q OR address ILIKE $q OR ...) - Combinação com outros filtros: AND com todos os filtros existentes
- Sanitização:
.strip()+ truncamento em 200 chars - Segurança: bind parameter do SQLAlchemy ORM — sem risco de SQL injection
Exemplo: ?q=Jardins&listing_type=aluguel retorna apenas imóveis de aluguel cujo título, endereço, código ou bairro contenha "Jardins".
Exemplo: ?q=AP-0042 retorna o imóvel com code = 'AP-0042' (ou qualquer imóvel com "AP-0042" no título/endereço).
sort — Ordenação
| Valor | ORDER BY gerado |
Comportamento |
|---|---|---|
relevance |
property.created_at DESC |
Mais recentes primeiro (comportamento anterior) |
price_asc |
property.price ASC |
Menor preço primeiro |
price_desc |
property.price DESC |
Maior preço primeiro |
area_desc |
property.area_m2 DESC |
Maior área primeiro |
newest |
property.created_at DESC |
Igual a relevance |
| (valor desconhecido) | property.created_at DESC |
Fallback para relevance |
Exemplos de Chamada
# Busca por bairro + tipo de negócio ordenado por preço
GET /api/v1/properties?q=Jardins&sort=price_asc&listing_type=aluguel&page=1&per_page=16
# Busca por código exato
GET /api/v1/properties?q=AP-0042
# Imóveis mais recentes de São Paulo
GET /api/v1/properties?sort=newest&city_id=1
# Menor área mínima + maior preço (combinação complexa)
GET /api/v1/properties?sort=price_desc&bedrooms_min=3&city_id=1&q=Pinheiros
Implementação Backend
# Em backend/app/routes/properties.py — adicionar antes da paginação
# ── Busca textual (q) ────────────────────────────────────────────────────────
q = args.get("q", "").strip()
if len(q) > 200:
q = q[:200]
if q:
from sqlalchemy import or_
from sqlalchemy.orm import aliased
from app.models.location import Neighborhood as NeighborhoodAlias
nbh_alias = aliased(NeighborhoodAlias)
query = query.outerjoin(nbh_alias, Property.neighborhood_id == nbh_alias.id)
pattern = f"%{q}%"
query = query.filter(or_(
Property.title.ilike(pattern),
Property.address.ilike(pattern),
Property.code.ilike(pattern),
nbh_alias.name.ilike(pattern),
))
# ── Ordenação (sort) — SUBSTITUIR o order_by existente ─────────────────────
sort = args.get("sort", "relevance")
sort_map = {
"price_asc": Property.price.asc(),
"price_desc": Property.price.desc(),
"area_desc": Property.area_m2.desc(),
"newest": Property.created_at.desc(),
}
order_clause = sort_map.get(sort, Property.created_at.desc())
# ── Paginação (existente — apenas mover order_by) ───────────────────────────
props = (
query.order_by(order_clause) # ← substituiu o .order_by(Property.created_at.desc()) fixo
.offset((page - 1) * per_page)
.limit(per_page)
.all()
)
Implementação Frontend
// services/properties.ts — adicionar ao PropertyFilters
q?: string
sort?: 'relevance' | 'price_asc' | 'price_desc' | 'area_desc' | 'newest'
// No getProperties():
if (filters.q?.trim()) params.q = filters.q.trim()
if (filters.sort && filters.sort !== 'relevance') params.sort = filters.sort
// PropertiesPage.tsx — filtersFromParams
q: get('q') ?? undefined,
sort: (get('sort') as SortOption) ?? undefined,
// PropertiesPage.tsx — filtersToParams
if (filters.q) p.set('q', filters.q)
if (filters.sort && filters.sort !== 'relevance') p.set('sort', filters.sort)