Encontre receitas a partir dos ingredientes que você já tem em casa.
- Visão geral
- Stack
- Arquitetura (monorepo)
- Decisões técnicas
- Rotas
- Regras de negócio
- Códigos de erro
- Setup rápido (Docker)
- Dev Container
- Variáveis de ambiente
- Fluxo de uso (exemplo)
- Como rodar os testes
- Integração contínua
- Melhorias futuras
- Autores
O PantryChef é uma aplicação web (SPA mobile-first) com API própria para busca e geração de receitas a partir dos ingredientes disponíveis em casa. Quando nenhuma receita cadastrada atende à busca, uma receita é gerada automaticamente (via OpenRouter), marcada como "criada pela IA" e persistida no catálogo.
- Cadastro e autenticação de usuários (JWT Bearer, senha com hash bcrypt).
- CRUD completo de receitas com ingredientes e quantidades.
- Busca por ingredientes (regra de subconjunto) e por nome/categoria.
- Geração automática de receitas quando a busca não encontra resultado no banco.
- Histórico de receitas visualizadas e favoritos, por usuário.
- Interface web: busca por ingredientes, catálogo com filtros, detalhe com compartilhamento, favoritos, histórico, perfil e criação/edição de receitas.
| Camada | Tecnologia |
|---|---|
| Linguagem | Python 3.11 |
| API | FastAPI |
| ORM | SQLAlchemy 2 |
| Migrações | Alembic |
| Banco de dados | PostgreSQL 15 |
| Geração de receitas | OpenAI SDK via OpenRouter |
| Frontend | React 19 + Vite + TypeScript, Tailwind CSS v4 + shadcn/ui, TanStack Query, React Router |
| Testes | Pytest (API) · Vitest + Testing Library + MSW (web) |
| Infraestrutura | Docker / Docker Compose |
O repositório segue uma estrutura de monorepo, separando os aplicativos e os pacotes compartilhados:
pantrychef/
├── apps/
│ ├── api/ # Backend (FastAPI)
│ │ ├── app/
│ │ │ ├── main.py # Instância FastAPI, registro de routers e /health
│ │ │ ├── config.py # Leitura das variáveis de ambiente (settings)
│ │ │ ├── database.py # engine, SessionLocal, Base, get_db
│ │ │ ├── dependencies.py # get_current_user, get_optional_user, get_recipe_generator
│ │ │ ├── exceptions.py # RecipeGenerationUnavailable
│ │ │ ├── models/ # Tabelas SQLAlchemy (Usuario, Receita, Ingrediente, ...)
│ │ │ ├── schemas/ # Modelos Pydantic de entrada/saída
│ │ │ ├── services/ # Regras de negócio, queries e integração externa
│ │ │ ├── routers/ # Camada HTTP (request → service → response_model)
│ │ │ ├── seeds/ # Carga inicial de dados (ingredientes em pt-BR)
│ │ │ └── utils/ # Utilitários (ex.: geração de slug)
│ │ ├── migrations/ # Versões do Alembic
│ │ ├── tests/ # Suíte de testes (pytest)
│ │ ├── Dockerfile
│ │ ├── entrypoint.sh # Aplica migrações e inicia o servidor
│ │ ├── alembic.ini
│ │ ├── pytest.ini
│ │ ├── requirements.txt # Dependências de produção
│ │ └── requirements-dev.txt # + ferramentas de teste
│ └── web/ # Frontend (React + Vite), servido por nginx no Docker
│ ├── src/app/ # Rotas, layouts e providers
│ ├── src/features/ # Telas por domínio (auth, recipes, favorites, history, profile)
│ ├── src/shared/ # Componentes compartilhados (navegação, card de receita...)
│ ├── src/types/ # Tipos de domínio (contratos da API)
│ ├── src/lib/ # Cliente HTTP e utilitários
│ └── nginx.conf # SPA + proxy /api → API, headers de segurança
├── packages/
│ └── shared-types/ # (placeholder) tipos compartilhados entre os apps
├── db/
│ └── dump.sql # Dados de exemplo (idempotente)
├── .github/workflows/ci.yml # Pipeline de CI
├── docker-compose.yml # Ambiente completo (db + api + frontend)
└── docker-compose.dev.yml # Override para desenvolvimento (reload, portas locais)
Cada app é autocontido (dependências, configurações e testes próprios), o que permite evoluir a API e o front-end de forma independente.
A integração usa o SDK da OpenAI apontado para a OpenRouter, um endpoint
compatível com OpenAI que centraliza dezenas de modelos de vários provedores. Trocar
de modelo é apenas alterar a variável AI_MODEL — sem mudar código nem SDK. O modelo
padrão openrouter/free roteia para modelos gratuitos, suficiente para demonstrações
sem custo.
O serviço de geração é injetado como dependência no router de busca, o que mantém a camada HTTP desacoplada da integração externa e permite substituí-lo facilmente em testes (mock) e em novos contextos de uso.
Falhas na geração de receitas (chave inválida, sem crédito, timeout) resultam em
HTTP 503 controlado, com uma retentativa antes de falhar. A API permanece estável e
informa o cliente de forma clara que o recurso externo está indisponível.
Criar receitas exige autenticação e vincula a receita ao autor. Editar ou remover é
permitido apenas ao autor (HTTP 403 para terceiros). As consultas permanecem
públicas: o catálogo é compartilhado.
Uma receita só é retornada se todos os seus ingredientes estiverem entre os informados na busca. Os resultados são ordenados por maior sobreposição de ingredientes, priorizando as receitas que mais aproveitam o que o usuário tem.
A busca por nomes (POST /recipes/search-by-name) resolve primeiro os ingredientes
já cadastrados; só os nomes desconhecidos são enviados à IA para validação. Assim a
busca continua funcionando com a IA fora do ar sempre que houver 3 ingredientes
conhecidos. O texto do usuário vai delimitado no prompt e a resposta da IA é
sanitizada antes de ser salva.
Limite de tentativas por IP no login, no cadastro e nas rotas que acionam IA (HTTP 429 com
Retry-After), JWT_SECRET obrigatório com no mínimo 32 caracteres, e-mails sem
distinção de maiúsculas e headers de segurança (CSP, X-Frame-Options...) no nginx.
| Método | Rota | Descrição |
|---|---|---|
| GET | /health |
Verificação de disponibilidade. |
| POST | /users |
Cadastro de usuário. |
| POST | /auth/login |
Autenticação e emissão de token (form-urlencoded). |
| GET | /ingredients |
Lista de ingredientes. |
| GET | /recipes |
Lista de receitas: filtros nome, categoria, tempo_min, tempo_max, dificuldade; ordenacao (recentes, populares, tempo_asc, tempo_desc, nome_asc, nome_desc); paginação limit/offset. |
| GET | /recipes/{receita_id} |
Detalhe de uma receita (?registrar=false não registra no histórico). |
| POST | /recipes/search |
Busca por IDs de ingredientes (mínimo de 3). |
| POST | /recipes/search-by-name |
Busca por nomes em texto livre (mínimo de 3, máximo de 10). |
| Método | Rota | Descrição |
|---|---|---|
| GET | /users/me |
Perfil do usuário autenticado. |
| PATCH | /users/me |
Atualização do perfil (nome, e-mail e/ou senha). |
| GET | /users/me/preferences |
Preferências (categorias favoritas e restrições). |
| PATCH | /users/me/preferences |
Atualização das preferências. |
| DELETE | /users/me |
Exclusão lógica da conta. |
| POST | /recipes |
Criação de receita (vinculada ao autor). |
| PUT | /recipes/{receita_id} |
Atualização de receita (apenas o autor). |
| DELETE | /recipes/{receita_id} |
Remoção de receita (apenas o autor). |
| GET | /history |
Histórico de receitas visualizadas. |
| GET | /favorites |
Lista de favoritos. |
| POST | /favorites |
Favoritar receita. |
| DELETE | /favorites/{receita_id} |
Remover favorito. |
Autoria das receitas: criar uma receita exige autenticação e a vincula ao usuário (
usuario_id). Editar ou remover é permitido apenas ao autor — outro usuário recebeHTTP 403. As consultas (GET /recipes,GET /recipes/{id}) e a busca (POST /recipes/search) permanecem públicas: o catálogo é compartilhado. Receitas geradas automaticamente no fallback ficam sem autor (catálogo global).
GET /recipes/{receita_id}é público, mas registra o acesso no histórico quando um token válido é enviado.
- RN-01: a busca por ingredientes exige no mínimo 3 ingredientes (
HTTP 422). - RN-02: a geração automática só é acionada quando não há receita correspondente no banco.
- RN-03: a receita gerada é persistida antes de ser retornada.
- RN-04: e-mail duplicado (sem distinção de maiúsculas) no cadastro ou na
edição do perfil retorna
HTTP 409. - RN-05: histórico e favoritos são estritamente por usuário autenticado.
- RN-06: criar receita exige autenticação; editar/remover é restrito ao autor
(
HTTP 403). - Busca por subconjunto: uma receita só é retornada se todos os seus ingredientes estiverem entre os informados (ordenada por maior sobreposição).
- Indisponibilidade da geração: falhas resultam em
HTTP 503controlado.
| Código | Significado |
|---|---|
401 |
Não autenticado / token inválido. |
403 |
Autenticado, mas sem permissão (ex.: editar receita de outro usuário). |
404 |
Recurso não encontrado. |
409 |
E-mail já cadastrado. |
422 |
Validação (ex.: menos de 3 ingredientes, ingrediente inexistente ou repetido). |
429 |
Muitas tentativas em pouco tempo (login, cadastro ou rotas de IA). |
503 |
Serviço externo de geração de receitas indisponível. |
-
Crie o arquivo
.enva partir do exemplo:cp .env.example .env
Defina um
JWT_SECRETcom pelo menos 32 caracteres (a API não sobe sem ele):openssl rand -hex 32. AAI_API_KEYsó é necessária para a geração de receitas. -
Suba todo o ambiente com um único comando:
docker compose up --build
- Aplicação web:
http://localhost:3000(a API fica atrás do nginx em/api) - Na subida, a API aplica as migrações e popula o catálogo de ingredientes
(desative com
SEED_INGREDIENTES=false). - No compose padrão, API e Postgres não ficam expostos no host.
- Aplicação web:
-
(Opcional) Carregue dados de exemplo — 1 usuário, 20 ingredientes e 3 receitas:
docker compose exec -T db psql -U postgres -d pantrychef < db/dump.sql
O override de desenvolvimento adiciona recarregamento automático (bind mount +
--reload) e publica API e Postgres apenas em 127.0.0.1:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build- Swagger:
http://localhost:8000/docs· ReDoc:http://localhost:8000/redoc - Frontend com hot reload (fora do Docker):
cd apps/web && npm ci && npm run dev(http://localhost:5173, comVITE_API_URL=http://localhost:8000).
As migrações são aplicadas automaticamente no boot do container. Para rodar manualmente dentro do container da API:
# aplicar todas as migrações
docker compose exec api alembic upgrade head
# criar uma nova migração a partir das alterações dos modelos
docker compose exec api alembic revision --autogenerate -m "descricao"O Alembic usa a variável DATABASE_URL do ambiente.
O esquema é sempre criado pelas migrações do Alembic. O arquivo
db/dump.sql contém apenas dados (1 usuário, 20 ingredientes e
3 receitas), é idempotente e resolve as referências por slug/e-mail — pode ser
aplicado a qualquer momento depois que a API subiu:
docker compose exec -T db psql -U postgres -d pantrychef < db/dump.sqlCredenciais do usuário de teste: ana@example.com / senha senha123.
O repositório inclui uma configuração de Dev Container (.devcontainer/) que
sobe o ambiente do docker compose com o override de desenvolvimento, com a pasta de trabalho apontando para a
raiz do monorepo (/workspaces/pantrychef) e as dependências da API instaladas
automaticamente. Para usar: abra o projeto no Visual Studio Code e escolha
Reabrir no Container. Extensões de Python, SQL e ferramentas de API já vêm
configuradas.
| Variável | Obrigatória | Descrição |
|---|---|---|
DATABASE_URL |
sim | URL de conexão do PostgreSQL. |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
sim | Credenciais do banco (usadas pelo container do Postgres). |
JWT_SECRET |
sim | Segredo usado para assinar os tokens JWT (mín. 32 caracteres; a API não inicia sem ele). |
JWT_EXPIRE_MINUTES |
não | Validade do token em minutos (padrão 60). |
AI_API_KEY |
só p/ geração | Chave da API da OpenRouter (sk-or-...). |
AI_BASE_URL |
não | Endpoint compatível com OpenAI (padrão https://openrouter.ai/api/v1). |
AI_MODEL |
não | Modelo de geração (padrão openrouter/free). |
AI_TIMEOUT |
não | Prazo total de cada tentativa de chamada à IA, em segundos (padrão 30). |
AI_MAX_TENTATIVAS |
não | Tentativas por chamada à IA; erros permanentes (ex.: chave inválida) não são repetidos (padrão 3). |
AI_MAX_TOKENS |
não | Tokens máximos por resposta da IA (padrão 4096). |
RATE_LIMIT_LOGIN |
não | Tentativas de login por IP na janela (padrão 10; 0 desativa). |
RATE_LIMIT_IA |
não | Buscas que podem acionar IA por IP na janela (padrão 15; 0 desativa). |
RATE_LIMIT_JANELA_SEGUNDOS |
não | Tamanho da janela dos limites de login e IA (padrão 60). |
RATE_LIMIT_CADASTRO |
não | Cadastros por IP na janela (padrão 10; 0 desativa). |
RATE_LIMIT_CADASTRO_JANELA_SEGUNDOS |
não | Janela do limite de cadastro (padrão 3600). |
CORS_ORIGINS |
não | Origens permitidas, separadas por vírgula (padrão: Vite em :5173). |
SEED_INGREDIENTES |
não | Popula o catálogo de ingredientes ao subir a API (padrão true). |
Usando curl com o override de desenvolvimento (base http://localhost:8000):
# 1. Cadastro (JSON)
curl -X POST http://localhost:8000/users \
-H "Content-Type: application/json" \
-d '{"nome":"Ana","email":"ana@example.com","senha":"senha123"}'
# 2. Login (form-urlencoded) — guarde o access_token retornado
curl -X POST http://localhost:8000/auth/login \
-d "username=ana@example.com" -d "password=senha123"
# 3. Listar ingredientes (pegue os UUIDs)
curl http://localhost:8000/ingredients
# 4. Criar receita (rota protegida — vincula a receita ao autor)
curl -X POST http://localhost:8000/recipes \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"nome":"Molho de Tomate","modo_preparo":"Cozinhe.","categoria":"Molho",
"ingredientes":[{"ingrediente_id":"<uuid>","quantidade":"3"}]}'
# 5. Buscar por ingredientes (mínimo de 3 UUIDs)
curl -X POST http://localhost:8000/recipes/search \
-H "Content-Type: application/json" \
-d '{"ingredientes":["<uuid1>","<uuid2>","<uuid3>"]}'
# 6. Favoritar (rota protegida)
curl -X POST http://localhost:8000/favorites \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"receita_id":"<uuid>"}'A suíte usa um PostgreSQL dedicado e o serviço de IA é sempre mockado (nenhuma
chamada externa real). Ela nunca usa o banco da aplicação: o banco de testes é
TEST_DATABASE_URL ou, na falta dele, o mesmo servidor do DATABASE_URL com o
sufixo _test (criado automaticamente). Bancos cujo nome não termina em _test
são recusados.
Com Docker (com o override de desenvolvimento no ar):
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec api pytest -qLocalmente (fora do Docker):
python -m venv .venv && source .venv/bin/activate
pip install -r apps/api/requirements-dev.txt
export TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5433/pantrychef_test
cd apps/api && pytest -qcd apps/web
npm ci
npm run test:run # testes unitários e de integração (MSW simula a API)
npm run test:coverage # relatório de cobertura em apps/web/coverage
npm run lint && npm run format:check && npm run buildO workflow .github/workflows/ci.yml roda em todo push
(qualquer branch) e em pull requests para develop e main:
- API: Postgres de serviço, ciclo completo de migrações e
pytest. - Web: lint (sem avisos), Prettier, testes com cobertura e build.
- Docker: build das imagens do compose.
O deploy automático ainda depende da escolha da plataforma de hospedagem
(issue #25). A imagem do frontend recebe a URL da API no build (VITE_API_URL,
padrão /api via proxy do nginx).
- Deploy automático (pendente da escolha de plataforma, issue #25).
- Tipos compartilhados gerados a partir do OpenAPI em
packages/shared-types. - Upload de fotos das receitas.
- Sugestões de receitas com base no histórico e nas preferências.
- Rate limit distribuído (ex.: Redis) caso a API rode com várias réplicas.
- Cache das consultas para reduzir latência das listagens.
João Barbosa e equipe — Projeto de cadeira de Engenharia de Software.