Aplicação didática demonstrando as melhores práticas de desenvolvimento REST API com Spring Boot
Documentação • Instalação • Endpoints • Exemplos • Arquitetura
Esta aplicação REST API foi desenvolvida com propósito 100% didático para demonstrar as melhores práticas de desenvolvimento Java com Spring Boot, seguindo os princípios SOLID e Clean Code.
- ✅ Arquitetura em Camadas: Controller → Service → DTO
- ✅ Validação de Dados: Bean Validation com Jakarta
- ✅ Tratamento de Exceções: Global Exception Handler
- ✅ Documentação: JavaDoc completo e README detalhado
- ✅ Padrões REST: Endpoints semânticos e status HTTP corretos
- ✅ Clean Code: Código limpo, legível e manutenível
- ✅ SOLID: Aplicação dos 5 princípios fundamentais
- ✅ HTTP Headers: Manipulação correta de headers
- ✅ Request/Response Patterns: DTOs padronizados
- Validação de dados com Jakarta Bean Validation
- DTOs (Data Transfer Objects) para Request/Response
- Anotações:
@Valid,@RequestBody,@NotBlank,@Size,@Min
- Path Variables: Parâmetros na URL (
/api/users/{id}) - Query Parameters: Parâmetros de consulta (
?page=1&size=10) - Validação de tipos e valores
- Captura de headers específicos e múltiplos
- Headers customizados na resposta
- Validação de headers obrigatórios
- Tratamento centralizado de exceções
- Respostas padronizadas de erro
- Validação em diferentes camadas
Backend Framework
├── Spring Boot 3.5.7 → Framework principal
├── Spring Web → REST API
├── Spring Validation → Jakarta Bean Validation
└── Spring Actuator → Monitoramento e métricas
Build & Dependency Management
├── Maven 3.9+ → Gerenciamento de dependências
└── Java 17 → Linguagem de programação
Tools & DevOps
├── Spring DevTools → Hot reload em desenvolvimento
├── Docker → Containerização
└── Git → Controle de versão
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Jakarta Bean Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Antes de começar, certifique-se de ter instalado:
| Ferramenta | Versão Mínima | Download |
|---|---|---|
| Java JDK | 17+ | Oracle JDK |
| Maven | 3.9+ | Apache Maven |
| Git | 2.0+ | Git SCM |
| IDE | - | IntelliJ IDEA ou VS Code |
# Verificar Java
java -version
# Saída esperada: openjdk version "17.x.x"
# Verificar Maven
mvn -version
# Saída esperada: Apache Maven 3.9.x
# Verificar Git
git --version
# Saída esperada: git version 2.x.xgit clone https://github.com/Cardosofiles/springboot-starter-server.git
cd springboot-starter-servermvn clean installmvn spring-boot:runA aplicação estará disponível em: http://localhost:8080
# Testar Health Check
curl http://localhost:8080/api/v1/healthmvn spring-boot:run# Compilar e gerar JAR
mvn clean package
# Executar JAR
java -jar target/springboot-starter-server-0.0.1.jar# Construir imagem
docker build -t springboot-api:latest .
# Executar container
docker run -p 8080:8080 --name api-server springboot-api:latest
# Verificar logs
docker logs -f api-server# Health Check
curl http://localhost:8080/api/v1/health
# Actuator Endpoints
curl http://localhost:8080/actuator
curl http://localhost:8080/actuator/healthEste projeto segue uma arquitetura em camadas (Layered Architecture) aplicando princípios SOLID e Clean Code.
┌─────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ (Browser, Mobile App, Postman) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ CONTROLLER LAYER │
│ • Recebe requisições HTTP │
│ • Valida entrada com @Valid │
│ • Delega para Service Layer │
│ • Retorna ResponseEntity com DTOs │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ • Contém lógica de negócio │
│ • Processa dados │
│ • Coordena operações │
│ • Independente de framework │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ DTO LAYER │
│ • Request DTOs (entrada) │
│ • Response DTOs (saída) │
│ • Validações com Jakarta Bean Validation │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ EXCEPTION HANDLER │
│ • Tratamento centralizado de erros │
│ • Respostas padronizadas │
│ • @RestControllerAdvice │
└─────────────────────────────────────────────────────────┘
| Princípio | Aplicação no Projeto |
|---|---|
| Single Responsibility | Cada classe tem uma única responsabilidade |
| Open/Closed | Extensível via herança, fechado para modificação |
| Liskov Substitution | DTOs e Services são substituíveis |
| Interface Segregation | Interfaces específicas para cada contexto |
| Dependency Inversion | Controllers dependem de abstrações (Services) |
springboot-starter-server/
│
├── src/
│ ├── main/
│ │ ├── java/com/cardosofiles/
│ │ │ │
│ │ │ ├── controller/ # 🎮 Camada de Controllers
│ │ │ │ ├── BodyRequestController.java → Requisições POST/PUT
│ │ │ │ ├── UrlParamsController.java → Path Variables & Query Params
│ │ │ │ ├── HeaderParamsController.java → HTTP Headers
│ │ │ │ └── HealthCheckController.java → Health Check
│ │ │ │
│ │ │ ├── service/ # 💼 Camada de Serviços
│ │ │ │ ├── UserService.java → Lógica de usuários
│ │ │ │ ├── UrlParamsService.java → Processamento de params
│ │ │ │ └── HeaderService.java → Processamento de headers
│ │ │ │
│ │ │ ├── dto/ # 📦 Data Transfer Objects
│ │ │ │ ├── request/
│ │ │ │ │ └── UserRequest.java → DTO de entrada
│ │ │ │ └── response/
│ │ │ │ ├── ApiResponse.java → DTO genérico de resposta
│ │ │ │ ├── HealthResponse.java → DTO health check
│ │ │ │ ├── QueryParamsResponse.java → DTO query params
│ │ │ │ └── HeaderInfoResponse.java → DTO headers
│ │ │ │
│ │ │ ├── exception/ # 🛡️ Tratamento de Exceções
│ │ │ │ └── GlobalExceptionHandler.java → Handler global
│ │ │ │
│ │ │ └── SpringbootStarterServerApplication.java # 🚀 Classe Principal
│ │ │
│ │ └── resources/
│ │ ├── application.properties # ⚙️ Configurações
│ │ └── application.yml # ⚙️ Configurações (alternativo)
│ │
│ └── test/ # 🧪 Testes Unitários
│ └── java/com/cardosofiles/
│
├── .github/
│ └── copilot-instructions.md # 🤖 Instruções Copilot
│
├── pom.xml # 📦 Dependências Maven
├── Dockerfile # 🐳 Configuração Docker
├── .gitignore # 🚫 Arquivos ignorados
└── README.md # 📖 Documentação (este arquivo)
Responsável por:
- Receber requisições HTTP
- Validar entrada com
@Valid - Delegar processamento para Services
- Retornar respostas padronizadas
Responsável por:
- Implementar lógica de negócio
- Processar dados
- Coordenar operações
- Ser independente de framework
Responsável por:
- Definir estrutura de dados
- Aplicar validações
- Documentar contratos de API
Responsável por:
- Capturar exceções globalmente
- Padronizar respostas de erro
- Retornar status HTTP apropriados
| Categoria | Endpoint Base | Descrição |
|---|---|---|
| 👤 Usuários | /api/v1/users |
Operações com body parameters |
| 🔗 Parâmetros | /api/v1/params |
Path variables e query params |
| 📋 Headers | /api/v1/headers |
Manipulação de HTTP headers |
| ❤️ Health | /api/v1/health |
Status da aplicação |
Cria um novo usuário.
Request Body:
{
"username": "john_doe",
"age": 25
}Response (201 Created):
{
"success": true,
"message": "Usuário criado com sucesso",
"data": {
"username": "john_doe",
"age": 25
},
"timestamp": "2024-01-15T10:30:00"
}Validações:
username: obrigatório, 3-50 caracteres, alfanuméricoage: obrigatório, >= 0
Atualiza dados de um usuário.
Request Body:
{
"username": "john_updated",
"age": 26
}Response (200 OK):
{
"success": true,
"message": "Usuário atualizado com sucesso",
"data": {
"username": "john_updated",
"age": 26
},
"timestamp": "2024-01-15T10:35:00"
}Busca recurso por ID via path variable.
Exemplo:
GET /api/v1/params/path/123Response (200 OK):
{
"success": true,
"message": "Recurso encontrado",
"data": "Recurso com ID: 123",
"timestamp": "2024-01-15T10:40:00"
}Validações:
id: deve ser um número positivo
Busca com query parameter simples.
Exemplo:
GET /api/v1/params/query?search=springResponse (200 OK):
{
"success": true,
"message": "Busca realizada com sucesso",
"data": "Resultado da busca por: spring",
"timestamp": "2024-01-15T10:45:00"
}Busca paginada com múltiplos query params.
Exemplo:
GET /api/v1/params/query/paginated?page=1&size=20&sort=nameResponse (200 OK):
{
"success": true,
"message": "Listagem realizada com sucesso",
"data": {
"page": 1,
"size": 20,
"sort": "name",
"message": "Página 1 com 20 itens, ordenado por: name"
},
"timestamp": "2024-01-15T10:50:00"
}Parâmetros:
page(opcional, padrão: 0): número da páginasize(opcional, padrão: 10): itens por páginasort(opcional): campo de ordenação
Captura todos os query parameters dinamicamente.
Exemplo:
GET /api/v1/params/query/all?filter=active&status=published&category=techResponse (200 OK):
{
"success": true,
"message": "Parâmetros processados com sucesso",
"data": {
"filter": "Processado: active",
"status": "Processado: published",
"category": "Processado: tech"
},
"timestamp": "2024-01-15T10:55:00"
}Captura um header específico.
Request Headers:
X-API-Key: abc123def456
Response (200 OK):
{
"success": true,
"message": "Header processado com sucesso",
"data": "API Key validada: abc123de...",
"timestamp": "2024-01-15T11:00:00"
}Captura múltiplos headers.
Request Headers:
User-Agent: Mozilla/5.0
Authorization: Bearer token123
Content-Type: application/json
Response (200 OK):
{
"success": true,
"message": "Headers processados com sucesso",
"data": {
"userAgent": "Mozilla/5.0",
"authorization": "Bearer ***",
"contentType": "application/json",
"allHeaders": null
},
"timestamp": "2024-01-15T11:05:00"
}Captura todos os headers da requisição.
Response (200 OK):
{
"success": true,
"message": "Todos os headers capturados",
"data": {
"userAgent": "PostmanRuntime/7.32.0",
"authorization": null,
"contentType": "application/json",
"allHeaders": {
"host": "localhost:8080",
"connection": "keep-alive",
"user-agent": "PostmanRuntime/7.32.0",
"accept": "*/*"
}
},
"timestamp": "2024-01-15T11:10:00"
}Define headers customizados na resposta.
Exemplo:
POST /api/v1/headers/response?customValue=myvalueResponse Headers:
X-Custom-Header: myvalue
X-Response-Time: 1705318200000
Response (200 OK):
{
"success": true,
"message": "Header de resposta configurado",
"data": "Valor customizado processado: myvalue",
"timestamp": "2024-01-15T11:15:00"
}Verifica o status da aplicação.
Response (200 OK):
{
"success": true,
"message": "Aplicação está funcionando corretamente",
"data": {
"status": "UP",
"timestamp": "2024-01-15T11:20:00",
"version": "1.0.0"
},
"timestamp": "2024-01-15T11:20:00"
}curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"age": 25
}'curl http://localhost:8080/api/v1/params/path/123curl "http://localhost:8080/api/v1/params/query/paginated?page=1&size=10&sort=name"curl http://localhost:8080/api/v1/headers/single \
-H "X-API-Key: my-secret-key-123"-
Importar Coleção
- Crie uma nova coleção chamada "Spring Boot API"
- Configure a variável
{{baseUrl}}=http://localhost:8080
-
Exemplo POST Request
Method: POST URL: {{baseUrl}}/api/v1/users Headers: Content-Type: application/json Body (raw JSON): { "username": "testuser", "age": 30 } -
Exemplo GET com Headers
Method: GET URL: {{baseUrl}}/api/v1/headers/multiple Headers: User-Agent: Postman/10.0 Authorization: Bearer mytoken123 Content-Type: application/json
import requests
# URL base
BASE_URL = "http://localhost:8080/api/v1"
# Criar usuário
def create_user():
response = requests.post(
f"{BASE_URL}/users",
json={"username": "pythonuser", "age": 28}
)
print(response.json())
# Buscar com query params
def search_paginated():
params = {"page": 1, "size": 10, "sort": "name"}
response = requests.get(f"{BASE_URL}/params/query/paginated", params=params)
print(response.json())
# Enviar headers customizados
def send_headers():
headers = {"X-API-Key": "python-key-123"}
response = requests.get(f"{BASE_URL}/headers/single", headers=headers)
print(response.json())
if __name__ == "__main__":
create_user()
search_paginated()
send_headers()import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
public class ApiClient {
private static final String BASE_URL = "http://localhost:8080/api/v1";
private final RestTemplate restTemplate = new RestTemplate();
// Criar usuário
public void createUser() {
String url = BASE_URL + "/users";
UserRequest request = new UserRequest("javauser", 30);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UserRequest> entity = new HttpEntity<>(request, headers);
ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
url, entity, ApiResponse.class
);
System.out.println(response.getBody());
}
// Buscar com path variable
public void getById() {
String url = BASE_URL + "/params/path/123";
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
url, ApiResponse.class
);
System.out.println(response.getBody());
}
}| Campo | Validação | Mensagem de Erro |
|---|---|---|
username |
@NotBlank |
"O nome de usuário é obrigatório" |
username |
@Size(min=3, max=50) |
"O nome de usuário deve ter entre 3 e 50 caracteres" |
username |
@Pattern(regexp="^[a-zA-Z0-9_-]+$") |
"O nome de usuário deve conter apenas letras, números, hífens e underscores" |
age |
@Min(0) |
"A idade deve ser maior ou igual a 0" |
| Parâmetro | Validação | Mensagem de Erro |
|---|---|---|
id |
@Positive |
"ID deve ser um número positivo" |
search |
@NotBlank |
"Parâmetro de busca não pode ser vazio" |
page |
@Positive |
"Página deve ser positiva" |
size |
@Positive |
"Tamanho deve ser positivo" |
| Header | Validação | Mensagem de Erro |
|---|---|---|
X-API-Key |
@NotBlank |
"API Key é obrigatória" |
Request Inválido:
{
"username": "ab",
"age": -5
}Response (400 Bad Request):
{
"success": false,
"message": "Erro de validação nos dados enviados",
"data": {
"username": "O nome de usuário deve ter entre 3 e 50 caracteres",
"age": "A idade deve ser maior ou igual a 0"
},
"timestamp": "2024-01-15T11:30:00"
}O projeto implementa um Global Exception Handler que captura e trata todos os erros de forma centralizada.
Causa: Dados inválidos no body, path variable ou query param
Exemplo:
{
"success": false,
"message": "Erro de validação nos dados enviados",
"data": {
"username": "O nome de usuário é obrigatório",
"age": "A idade deve ser maior ou igual a 0"
},
"timestamp": "2024-01-15T11:35:00"
}Causa: Tipo de dado incompatível (ex: string quando esperado número)
Request:
GET /api/v1/params/path/abcResponse:
{
"success": false,
"message": "O parâmetro 'id' deve ser do tipo Long",
"data": null,
"timestamp": "2024-01-15T11:40:00"
}Request sem header:
GET /api/v1/headers/singleResponse:
{
"success": false,
"message": "Header obrigatório ausente: X-API-Key",
"data": null,
"timestamp": "2024-01-15T11:45:00"
}Response:
{
"success": false,
"message": "Erro interno do servidor",
"data": "Descrição detalhada do erro",
"timestamp": "2024-01-15T11:50:00"
}| Status | Código | Significado | Quando Usar |
|---|---|---|---|
| ✅ OK | 200 | Sucesso | GET, PUT bem-sucedidos |
| ✅ Created | 201 | Criado | POST bem-sucedido |
| ❌ Bad Request | 400 | Requisição inválida | Erro de validação |
| ❌ Not Found | 404 | Não encontrado | Recurso inexistente |
| ❌ Internal Server Error | 500 | Erro no servidor | Erro não tratado |
// ❌ Evitar
public String m1(String s) { ... }
// ✅ Fazer
public String createUser(String username) { ... }// ❌ Evitar - função faz muitas coisas
public void processUser(UserRequest request) {
validate(request);
save(request);
sendEmail(request);
log(request);
}
// ✅ Fazer - cada função tem uma responsabilidade
public void processUser(UserRequest request) {
userService.createUser(request);
}// ❌ Evitar comentários óbvios
// Retorna o nome do usuário
public String getUsername() { ... }
// ✅ Fazer - código auto-explicativo
public String getUsername() { ... }
// ✅ Comentar lógica complexa quando necessário
// Aplica algoritmo de criptografia AES-256 com salt aleatório
public String encryptPassword(String password) { ... }Cada classe tem uma única responsabilidade.
// ✅ Controller - apenas recebe requisições
@RestController
public class UserController {
private final UserService userService;
// ...
}
// ✅ Service - apenas lógica de negócio
@Service
public class UserService {
public UserRequest createUser(UserRequest request) {
// lógica de negócio
}
}Aberto para extensão, fechado para modificação.
// ✅ Extensível através de herança/interfaces
public abstract class BaseService {
protected void logOperation(String operation) {
// implementação base
}
}
public class UserService extends BaseService {
// Pode estender sem modificar a base
}Dependa de abstrações, não de implementações.
// ✅ Controller depende da abstração (interface/service)
@RestController
public class UserController {
private final UserService userService; // abstração
public UserController(UserService userService) {
this.userService = userService;
}
}| Verbo | Uso | Exemplo |
|---|---|---|
| GET | Buscar dados | GET /api/v1/users |
| POST | Criar recurso | POST /api/v1/users |
| PUT | Atualizar completo | PUT /api/v1/users/{id} |
| PATCH | Atualizar parcial | PATCH /api/v1/users/{id} |
| DELETE | Remover | DELETE /api/v1/users/{id} |
# ✅ Fazer
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/123
# ❌ Evitar
GET /api/v1/getUsers
POST /api/v1/createUser
GET /api/v1/getUserById?id=123# ✅ Versão na URL
/api/v1/users
/api/v2/users
# ✅ Versão no header
Accept: application/vnd.api.v1+json{
"success": true,
"message": "Operação realizada com sucesso",
"data": {
/* dados */
},
"timestamp": "2024-01-15T12:00:00"
}# Application
spring.application.name=springboot-starter-server
server.port=8080
# Actuator
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
# Logging
logging.level.root=INFO
logging.level.com.cardosofiles=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Encoding
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true# filepath: /home/cardosofiles/www/java/developement/springboot-starter-server/Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/springboot-starter-server-0.0.1.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# filepath: /home/cardosofiles/www/java/developement/springboot-starter-server/docker-compose.yml
version: "3.8"
services:
api:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
restart: unless-stoppedmvn testmvn clean test jacoco:report# Abrir relatório no navegador
open target/site/jacoco/index.html# Compilar aplicação
mvn clean package -DskipTests
# Construir imagem Docker
docker build -t springboot-api:1.0.0 .
# Verificar imagem criada
docker images | grep springboot-api# Executar em primeiro plano
docker run -p 8080:8080 springboot-api:1.0.0
# Executar em background
docker run -d -p 8080:8080 --name api-server springboot-api:1.0.0
# Ver logs
docker logs -f api-server
# Parar container
docker stop api-server
# Remover container
docker rm api-server# Iniciar serviços
docker-compose up -d
# Ver logs
docker-compose logs -f
# Parar serviços
docker-compose down
# Rebuild e restart
docker-compose up -d --build- Clean Code - Robert C. Martin
- Design Patterns - Gang of Four
- Spring in Action - Craig Walls
- Effective Java - Joshua Bloch
Contribuições são sempre bem-vindas! Este é um projeto didático e melhorias são incentivadas.
-
Fork o projeto
git clone https://github.com/Cardosofiles/springboot-starter-server.git
-
Crie uma branch para sua feature
git checkout -b feature/nova-funcionalidade
-
Commit suas mudanças
git commit -m 'feat: adiciona nova funcionalidade' -
Push para a branch
git push origin feature/nova-funcionalidade
-
Abra um Pull Request
Seguimos o Conventional Commits:
feat:nova funcionalidadefix:correção de bugdocs:documentaçãostyle:formataçãorefactor:refatoraçãotest:testeschore:tarefas gerais
- Código segue os padrões do projeto
- Testes passando
- Documentação atualizada
- Commits seguem o padrão
- GitHub: @Cardosofiles
- LinkedIn: Cardosofiles
- Email: contato@cardosofiles.com
- Spring Team pela excelente documentação
- Comunidade Java pelo suporte
- Todos que contribuíram com este projeto
⭐ Se este projeto te ajudou, considere dar uma estrela!
📚 Feito com ❤️ para fins educacionais