5.1 KiB
5.1 KiB
REST API Contracts: Property Detail Page (004)
Feature: 004-property-detail-page
Date: 2026-04-13
Base URL: /api/v1
GET /properties/{slug}
Retorna o detalhe completo de um imóvel ativo pelo slug.
Request
GET /api/v1/properties/{slug}
Content-Type: application/json
Auth: não requerida
Path parameters:
| Param | Tipo | Descrição |
|---|---|---|
slug |
string [a-z0-9-]+ |
Slug único do imóvel |
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Apartamento 3 quartos no Centro",
"slug": "apartamento-3-quartos-centro-123",
"code": "AP-00042",
"description": "Excelente apartamento com vista para o jardim...",
"address": "Rua das Flores, 100, Centro",
"price": "850000.00",
"condo_fee": "650.00",
"type": "venda",
"bedrooms": 3,
"bathrooms": 2,
"parking_spots": 2,
"area_m2": 95,
"is_featured": true,
"subtype": { "id": 10, "name": "Apartamento", "slug": "apartamento" },
"city": { "id": 5, "name": "Franca", "slug": "franca", "state": "SP" },
"neighborhood": { "id": 12, "name": "Jardim São Luiz", "slug": "jardim-sao-luiz" },
"photos": [
{ "url": "https://...", "alt_text": "Sala de estar", "display_order": 0 },
{ "url": "https://...", "alt_text": "Quarto principal", "display_order": 1 }
],
"amenities": [
{ "id": 3, "name": "Aceita animais", "slug": "aceita-animais", "group": "caracteristica" },
{ "id": 7, "name": "Piscina", "slug": "piscina", "group": "lazer" }
]
}
Notas do schema:
priceecondo_feesão strings de decimal (ex:"850000.00") — useparseFloat()no frontendtypeé o campo canônico (equivale alisting_typena documentação da spec)photosordenadas pordisplay_orderASC (índice 0 = foto principal do carrossel)codeedescriptionsãonullse não preenchidos no cadastroaddressénullse não preenchido
Response 404 Not Found
Imóvel inexistente ou is_active = false.
{ "error": "Imóvel não encontrado" }
Retornar 404 (não 403) para imóveis inativos evita vazar informação sobre existência.
POST /properties/{slug}/contact
Registra um lead de contato vinculado ao imóvel identificado pelo slug.
Request
POST /api/v1/properties/{slug}/contact
Content-Type: application/json
Auth: não requerida
Path parameters:
| Param | Tipo | Descrição |
|---|---|---|
slug |
string | Slug do imóvel para o qual o contato é enviado |
Request body:
{
"name": "João Silva",
"email": "joao@email.com",
"phone": "(16) 99999-0000",
"message": "Tenho interesse no imóvel, gostaria de agendar uma visita."
}
| Campo | Tipo | Obrigatório | Restrições |
|---|---|---|---|
name |
string | sim | 2–150 caracteres |
email |
string | sim | formato e-mail válido (RFC 5322) |
phone |
string | não | máximo 20 caracteres |
message |
string | sim | 10–2000 caracteres |
Response 201 Created
{
"id": 88,
"message": "Mensagem enviada com sucesso!"
}
Response 404 Not Found
Slug não encontrado ou imóvel inativo.
{ "error": "Imóvel não encontrado" }
Response 422 Unprocessable Entity
Falha de validação Pydantic.
{
"error": "Dados inválidos",
"details": {
"email": ["E-mail inválido"],
"message": ["Campo obrigatório"]
}
}
Comportamentos Implícitos
| Situação | Comportamento |
|---|---|
slug com caracteres inválidos (maiúsculas, /, etc.) |
404 (SQLAlchemy não encontra correspondência) |
property_id na tabela contact_leads quando imóvel é deletado |
SET NULL (lead preservado) |
| Duplo envio de formulário | Dois registros criados (UI bloqueia durante submitting) |
photos vazia |
Array [] retornado; frontend exibe placeholder |
amenities vazia |
Array [] retornado; frontend omite seção de amenidades |
Integração Frontend
getProperty(slug: string): Promise<PropertyDetail>
// services/properties.ts
export async function getProperty(slug: string): Promise<PropertyDetail> {
const response = await api.get<PropertyDetail>(`/properties/${slug}`)
return response.data
}
404→ Axios lançaAxiosErrorcomstatus 404→ frontend exibe estado "não encontrado"- Outros erros → propagar para tratamento genérico
submitContactForm(slug: string, data: ContactFormData): Promise<{ id: number; message: string }>
// services/properties.ts
export async function submitContactForm(
slug: string,
data: ContactFormData,
): Promise<{ id: number; message: string }> {
const response = await api.post<{ id: number; message: string }>(
`/properties/${slug}/contact`,
data,
)
return response.data
}
201→ retorna{ id, message }422→AxiosErrorcomresponse.data.detailsdisponível para mapear erros por campo5xx→ exibir mensagem genérica sem apagar dados do formulário