Este projeto consiste no desenvolvimento de uma API backend para gestão de usuários, restaurantes e itens de cardápio em uma plataforma compartilhada entre restaurantes.
O sistema foi desenvolvido como parte do Tech Challenge – Fase 2 do programa de Pós-Graduação em Arquitetura e Desenvolvimento Java (FIAP), com foco na aplicação prática dos conceitos estudados ao longo do curso.
- Desenvolver uma API REST para gerenciamento de usuários, restaurantes e itens de cardápio
- Implementar operações CRUD para os módulos de User, Restaurant e Menu
- Aplicar os princípios de Clean Architecture para promover separação de responsabilidades, baixo acoplamento e alta coesão entre as camadas da aplicação
- Garantir qualidade de código com testes unitários e de integração
- Utilizar PostgreSQL como banco de dados relacional
- Containerizar a aplicação utilizando Docker
A aplicação segue os princípios de Clean Architecture, sendo organizada em módulos: User, Restaurant e Menu.
A estrutura é dividida em camadas principais, com isolamento entre domínio e infraestrutura:
-
Core (Domínio): Responsável pelas regras de negócio, independente de frameworks:
- domain → entidades e regras
- usecase → lógica da aplicação
- gateway → interfaces (contratos)
- exception → exceções de domínio
-
Infra (Infraestrutura): Responsável pelos detalhes técnicos:
- controller → endpoints REST
- persistence → acesso ao banco de dados (JPA)
- entity → mapeamento JPA
Essa separação garante baixo acoplamento, alta testabilidade e facilidade de manutenção e evolução.
classDiagram
direction TB
class Address {
-Long id
-String street
-String number
-String neighborhood
-String city
-String zipCode
}
class User {
<<abstract>>
-Long id
-String name
-String email
}
class Client {
}
class Owner {
-List~Restaurant~ restaurants
}
class Restaurant {
-String name
-Address address
-KitchenTypeEnum kitchenType
-String openingHours
-List~MenuItem~ menu
}
class MenuItem {
-Long id
-String name
-String description
-BigDecimal price
-Boolean onlyLocalConsumption
-String photoPath
}
User <|-- Client
User <|-- Owner
User "1" --> "1" Address
Owner "1" --> "1..*" Restaurant
Restaurant "1" --> "1" Address
Restaurant --> "1..*" MenuItem
erDiagram
USER {
BIGINT id PK
VARCHAR name
VARCHAR email
VARCHAR user_type
}
ADDRESS {
BIGINT id PK
VARCHAR street
VARCHAR number
VARCHAR city
VARCHAR neighborhood
VARCHAR zip_code
}
RESTAURANT {
BIGINT id PK
VARCHAR name
VARCHAR kitchen_type
VARCHAR opening_hours
BIGINT address_id FK
BIGINT owner_id FK
}
MENU_ITEM {
BIGINT id PK
VARCHAR name
VARCHAR description
DECIMAL price
BOOLEAN only_local_consumption
VARCHAR photo_path
BIGINT restaurant_id FK
}
USER ||--|| ADDRESS : has
USER ||--o{ RESTAURANT : owns
RESTAURANT ||--|| ADDRESS : located_at
RESTAURANT ||--o{ MENU_ITEM : contains
| Endpoint | Método | Descrição |
|---|---|---|
| /users | POST | Criar usuário |
| /users/{id} | GET | Obter usuário por ID |
| /users | GET | Listar usuários |
| /users/{id} | PUT | Atualizar usuário |
| /users/{id} | DELETE | Excluir usuário |
| Endpoint | Método | Descrição |
|---|---|---|
| /restaurants | POST | Criar restaurante |
| /restaurants/{id} | GET | Obter restaurante por ID |
| /restaurants | GET | Listar restaurantes |
| /restaurants/{id} | PATCH | Atualizar restaurante |
| /restaurants/{id} | DELETE | Excluir restaurante |
| Endpoint | Método | Descrição |
|---|---|---|
| /menu/{restaurantId} | POST | Adicionar itens ao menu |
| /menu/ | GET | Obter todos os menus |
| /menu/{restaurantId} | GET | Obter menu do restaurante |
| /menu/{restaurantId}/{menuItemId} | PUT | Atualizar item do menu |
| /menu/{restaurantId}/{menuItemId} | DELETE | Excluir item do menu |
Request
POST /users
{
"email": "joao@teste.com",
"name": "João",
"type": "OWNER"
}Response - Sucesso
{
"id": 1,
"name": "João",
"email": "joao@teste.com",
"userType": "OWNER",
"restaurants": null
}Request
PUT /users/1
{
"name": "Joao Pedro",
"userType": "OWNER"
}Request
POST /restaurants
Header: x-user-id: 1
{
"name": "Bean Burger",
"address": {
"id": null,
"street": "Avenida Paulista",
"number": "1000",
"neighborhood": "Bela Vista",
"city": "Sao Paulo",
"zipCode": "12345-123"
},
"kitchenType": "Brazilian",
"openingHours": "seg-sab: 10:00 - 23:59, dom: 10:00 - 21:59"
}Response - Sucesso
{
"id": 1,
"name": "Bean Burger",
"kitchenType": "BRAZILIAN"
}Erro: Usuário não é do tipo Owner
{
"type": "https://api.example.com/errors/restaurant.actionNotAllowed",
"title": "restaurant.actionNotAllowed",
"status": 403,
"detail": "Only users with OWNER role can create restaurants",
"instance": "/restaurants",
"code": "restaurant.actionNotAllowed",
"timestamp": "2026-03-24T17:04:50.664821775Z"
}Request
PATCH /restaurants/1
Header: x-user-id: 1
{
"kitchenType": "Italia"
}Erro - Tipo de cozinha inválido
{
"type": "https://api.example.com/errors/restaurant.invalidRequest",
"title": "restaurant.invalidRequest",
"status": 400,
"detail": "Invalid kitchen type value: Italia",
"instance": "/restaurants/1",
"code": "restaurant.invalidRequest",
"timestamp": "2026-03-24T17:07:12.073776365Z"
}Request
POST /menu/1
Header: x-user-id: 1
[
{
"name": "Burger",
"description": "Delicious burger",
"price": 25.90,
"isOnlyLocalConsumption": false,
"photoPath": "/photos/burger.jpg"
},
{
"name": "Fries",
"description": "Delicious fries",
"price": 5.90,
"isOnlyLocalConsumption": false,
"photoPath": "/photos/fries.jpg"
}
]Response - Sucesso
{
"ids": [1, 2]
}Erro - Item ja existente
{
"type": "https://api.example.com/errors/user.actionNotAllowed",
"title": "user.actionNotAllowed",
"status": 400,
"detail": "Item with this name already exists",
"instance": "/menu/1",
"code": "user.actionNotAllowed",
"timestamp": "2026-03-24T18:03:31.314716661Z"
}Erro - Usuário não é o dono do restaurante
{
"type": "https://api.example.com/errors/user.actionNotAllowed",
"title": "user.actionNotAllowed",
"status": 403,
"detail": "Action not allowed",
"instance": "/menu/1",
"code": "user.actionNotAllowed",
"timestamp": "2026-03-24T18:06:16.983010272Z"
}Request
PUT /menu/1/1
Header: x-user-id: 1
{
"name": "X-Burger",
"description": "Delicious x-burger",
"price": 29.90,
"isOnlyLocalConsumption": false,
"photoPath": "/photos/xburger.jpg"
}Response - Sucesso
{
"name": "X-Burger",
"description": "Delicious x-burger",
"price": 29.90,
"isOnlyLocalConsumption": false,
"photoPath": "/photos/xburger.jpg",
"restaurantId": 1
}Erro - Item não encontrado
{
"type": "https://api.example.com/errors/menuItem.menuItemNotFound",
"title": "menuItem.menuItemNotFound",
"status": 404,
"detail": "Menu Item not found",
"instance": "/menu/1/5",
"code": "menuItem.menuItemNotFound",
"timestamp": "2026-03-24T18:22:26.929020327Z"
}A aplicação utiliza Docker para garantir um ambiente padronizado e facilitar a execução.
- Docker
- Docker Compose
1. Subir os containers (primeira execução ou após mudanças)
docker-compose up --build2. Execuções futuras (sem rebuild)
docker-compose up3. Parar a aplicação
docker-compose downServiços
- app → API Spring Boot
- postgres → Banco de dados PostgreSQL
Acessos
- API: http://localhost:8080
- Banco: localhost:5433
Persistência
O banco de dados utiliza um volume Docker: postgres_data
- Use Cases testados com Mockito
- Validação das regras de negócio
- Controllers testados com MockMvc
- Integração com banco de dados configurado para testes
A collection para testes da API está disponível no repositório:
https://github.com/HugoOliveiraSoares/tech-challenge-ii
Importar Coleção:
- Clique em Import > File,
- Selecione o arquivo:
Tech challenge II.postman_collection.json