Sistema interno para gestão de empreendimentos imobiliários.
Monorepo com API Laravel (PHP) + frontend React (TypeScript) + PostgreSQL, executado via Docker.
- Clone o repositório:
git clone git@github.com:Frompaje/capital-test-dev-fullstack.git- Entre na pasta do projeto:
cd capital-test-dev-fullstack- Suba a aplicação:
docker compose up -d --buildNa primeira subida o Docker já faz todo o bootstrap: cria o .env do Laravel, instala dependências (Composer e npm), gera a APP_KEY, roda migrations e seeders. Aguarde ~1–2 minutos e acesse:
| Recurso | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend | http://localhost:8000/api |
Para acompanhar o bootstrap:
docker compose logs -f appCustomização de .env e comandos manuais estão na seção Setup técnico abaixo.
/
├── backend/ # API Laravel
├── frontend/ # Aplicação React + TypeScript (Vite)
├── docker/ # Dockerfile PHP, entrypoint e Nginx
├── docker-compose.yml
├── .env.example
└── README.md
Os valores padrão do docker-compose.yml já funcionam sem criar .env. Para customizar portas ou banco:
cp .env.example .envVariáveis disponíveis no .env da raiz:
| Variável | Padrão | Descrição |
|---|---|---|
APP_PORT |
8000 |
Porta do backend (Nginx) |
FRONTEND_PORT |
5173 |
Porta do frontend (Vite) |
API_URL |
http://localhost:8000/api |
URL da API usada pelo frontend |
DB_DATABASE |
capital_db |
Nome do banco |
DB_USERNAME |
capital_user |
Usuário do PostgreSQL |
DB_PASSWORD |
capital_pass |
Senha do PostgreSQL |
DB_EXTERNAL_PORT |
5432 |
Porta do PostgreSQL no host |
# Backend (Composer)
docker compose exec app composer install --no-interaction --prefer-dist --optimize-autoloader
# Frontend (npm)
docker compose exec frontend npm installdocker compose exec app php artisan migrate --forceRecriar o banco do zero (apaga os dados):
docker compose exec app php artisan migrate:freshdocker compose exec app php artisan db:seed --forceRecriar o banco e popular com dados de exemplo:
docker compose exec app php artisan migrate:fresh --seed| Serviço | Container | Função |
|---|---|---|
postgres |
capital_postgres |
Banco PostgreSQL 16 |
app |
capital_app |
PHP-FPM (Laravel) |
nginx |
capital_nginx |
Proxy HTTP da API |
frontend |
capital_frontend |
Vite (React) |
| Método | Endpoint | Descrição |
|---|---|---|
GET |
/api/enterprises |
Lista (suporta name, status, page, per_page) |
GET |
/api/enterprises/{id} |
Detalhe |
POST |
/api/enterprises |
Cadastro |
PUT |
/api/enterprises/{id} |
Atualização |
DELETE |
/api/enterprises/{id} |
Exclusão |
Status possíveis: em_lancamento, em_obras, entregue.
Base: http://localhost:8000/api
http://localhost:8000/api/enterprises
http://localhost:8000/api/enterprises?name=Residencial
http://localhost:8000/api/enterprises?status=em_obras
http://localhost:8000/api/enterprises?name=Parque&status=em_lancamento&page=1&per_page=10
http://localhost:8000/api/enterprises/{id}
http://localhost:8000/api/enterprises
{
"name": "Residencial Aurora",
"city": "São Paulo",
"state": "SP",
"total_value": 15000000,
"units_quantity": 60,
"unit_value": 250000,
"status": "em_lancamento"
}http://localhost:8000/api/enterprises/{id}
{
"name": "Residencial Aurora Atualizado",
"city": "Campinas",
"state": "SP",
"total_value": 18000000,
"units_quantity": 72,
"unit_value": 250000,
"status": "em_obras"
}http://localhost:8000/api/enterprises/{id}
- Acesse http://localhost:5173.
- Na listagem, use a busca por nome e as abas de status.
- Abra um empreendimento para ver os detalhes.
- Cadastre um novo empreendimento pelo botão de criação.
- Edite um registro existente.
- Exclua um empreendimento e confirme a remoção na listagem.
Não há autenticação neste módulo — o acesso é aberto, conforme o escopo do desafio.
- Monorepo simples: backend e frontend no mesmo repositório para facilitar a avaliação e o setup.
- Bootstrap no entrypoint:
composer install,key:generate,migrateeseedrodam ao subir o containerapp. - API REST com resource routes:
apiResourcedo Laravel para o CRUD padrão. - UUID como identificador: evita exposição sequencial de IDs.
- Status como string tipada: valores estáveis na API (
em_lancamento,em_obras,entregue) com labels em português no frontend. - Defaults hardcoded no Compose (ambiente de teste): senha do banco, portas e URL da API têm valores padrão no
docker-compose.ymlpara o avaliador subir com um único comando, sem criar.env. Em produto real, esses valores seriam obrigatoriamente via.env/secrets — o.envnunca iria para o versionamento.
- axios: instância única em
frontend/src/lib/axios.tscomVITE_API_URLe headers JSON; os services usam essa base e o React Query orquestra cache/mutações. - react-hook-form + zod: schema (
enterpriseFormSchema) viazodResolvertipa o form e valida formato no cliente; erros de domínio do servidor (422) são mapeados nos mesmos campos. - react-router-dom: rotas de listagem/detalhe/edição e filtros da listagem em query params (
useSearchParams), não só emuseState. - Tailwind + shadcn/ui: layout utilitário + primitives acessíveis; componentes de domínio (listagem, filtros, modais) compostam esses primitives.
- React Query + Axios: cache/invalidação da listagem e mutações sem estado global manual.
Sem debounce, cada tecla atualizaria a URL e dispararia um request (“R”, “Re”, “Res”…), gerando carga, race conditions e flicker.
A paginação foi implementada mostrando os valores numéricos das páginas diretamente na UI, não só os botões de avançar/voltar — o usuário vê 1 2 3 4 5 e pode clicar direto na página que quer, sem precisar navegar sequencialmente uma por uma.
Benefícios: link compartilhável (/?name=Residencial&status=em_obras&page=2), histórico do navegador e refresh mantêm o contexto. Mudança de name ou status remove page, evitando página inválida após filtrar.
- Cliente (Zod): presença, tamanho, UF, números
gt(0)/min(1)e enum de status — feedback imediato de formato. Não valida unicidade de nome no Zod (exigiria hit na API a cada digitação). - Servidor (DTO Laravel):
unique:enterprises,nameno store/update; responde 422 com{ message, errors }. - Formulário:
applyServerErrorscola o 422 no camponameviasetError. Se houver erros de campo, não dispara toast — o feedback fica no form.
O layout inicial foi prototipado com Lovable como referência visual. A implementação versionada é React (Vite + Tailwind + shadcn), adaptada — não o export bruto do prototipador. link - https://lovable.dev/preview/e5ToRKmYntDnBZy2SP2V2u95nALv9lfQ
Create/update/delete usam toast.success / toast.error + invalidação do React Query. Erro 422 com campos não gera toast — o formulário mostra o erro no campo. Preferível a alert() porque não bloqueia a UI.
- Formulário:
grid-cols-1→sm:grid-cols-2. - Filtro:
flex flex-wrappara busca e abas empilharem em viewports menores. - Tabela:
table-fixed+colgroupcom larguras percentuais +truncatenas células; scroll horizontal quando necessário.
ValidatesEnterpriseRules reutiliza regras numéricas (total_value > 0, units_quantity >= 1, unit_value > 0) no EnterpriseService, reforçando o DTO HTTP sem herança forçada entre services.
Controllers não passam $request->all() ao service. DTOs final readonly (StoreEnterpriseDto, UpdateEnterpriseDto, ListEnterpriseDto, EnterpriseResponseDto) validam na construção (fromArray) e tipam o contrato por camada. Fluxo do store: Controller → DTO → Service → Eloquent → EnterpriseResponseDto.
Não utilizamos Repository pelo fato de o próprio framework já fornecer os Query Scopes. Também não usamos interface no Repository, pois não há necessidade de trocar de ORM — o intuito de usar um framework opinado como o Laravel é justamente aproveitar o que ele já fornece.
EnterpriseServiceInterface define o contrato; EnterpriseService implementa; binding no AppServiceProvider. O controller injeta a interface (Dependency Inversion), facilitando mock em testes e deixando o contrato público explícito.
Se o produto crescesse, o próximo passo arquitetural seria organizar o projeto por módulos de domínio, ao invés de uma estrutura única com todos os Controllers e Services misturados. Exemplo: um módulo user conteria seu próprio Controller, Service, Interface (contrato do Service) e Traits, isolado do módulo enterprise, que seguiria a mesma estrutura. Isso facilita escalar o time por domínio, isolar testes por módulo, e evita que uma pasta única vire um emaranhado de arquivos sem relação clara entre si.
# Parar os containers
docker compose down
# Parar e remover o volume do banco
docker compose down -v
# Recriar o banco do zero
docker compose exec app php artisan migrate:fresh --seed
# Reinstalar dependências do frontend
docker compose exec frontend npm install- PHP 8.3 / Laravel
- React 19 / TypeScript / Vite
- PostgreSQL 16
- Docker / Docker Compose
- Nginx